github.com/jincm/wesharechain@v0.0.0-20210122032815-1537409ce26a/chain/swarm/network/stream/delivery_test.go (about) 1 // Copyright 2018 The go-ethereum Authors 2 // This file is part of the go-ethereum library. 3 // 4 // The go-ethereum library is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU Lesser General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // The go-ethereum library is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU Lesser General Public License for more details. 13 // 14 // You should have received a copy of the GNU Lesser General Public License 15 // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. 16 17 package stream 18 19 import ( 20 "bytes" 21 "context" 22 "errors" 23 "fmt" 24 "sync" 25 "testing" 26 "time" 27 28 "github.com/ethereum/go-ethereum/node" 29 "github.com/ethereum/go-ethereum/p2p" 30 "github.com/ethereum/go-ethereum/p2p/enode" 31 "github.com/ethereum/go-ethereum/p2p/protocols" 32 "github.com/ethereum/go-ethereum/p2p/simulations/adapters" 33 p2ptest "github.com/ethereum/go-ethereum/p2p/testing" 34 "github.com/ethereum/go-ethereum/swarm/log" 35 "github.com/ethereum/go-ethereum/swarm/network" 36 pq "github.com/ethereum/go-ethereum/swarm/network/priorityqueue" 37 "github.com/ethereum/go-ethereum/swarm/network/simulation" 38 "github.com/ethereum/go-ethereum/swarm/state" 39 "github.com/ethereum/go-ethereum/swarm/storage" 40 "github.com/ethereum/go-ethereum/swarm/testutil" 41 ) 42 43 //Tests initializing a retrieve request 44 func TestStreamerRetrieveRequest(t *testing.T) { 45 regOpts := &RegistryOptions{ 46 Retrieval: RetrievalClientOnly, 47 Syncing: SyncingDisabled, 48 } 49 tester, streamer, _, teardown, err := newStreamerTester(regOpts) 50 if err != nil { 51 t.Fatal(err) 52 } 53 defer teardown() 54 55 node := tester.Nodes[0] 56 57 ctx := context.Background() 58 req := network.NewRequest( 59 storage.Address(hash0[:]), 60 true, 61 &sync.Map{}, 62 ) 63 streamer.delivery.RequestFromPeers(ctx, req) 64 65 stream := NewStream(swarmChunkServerStreamName, "", true) 66 67 err = tester.TestExchanges(p2ptest.Exchange{ 68 Label: "RetrieveRequestMsg", 69 Expects: []p2ptest.Expect{ 70 { //start expecting a subscription for RETRIEVE_REQUEST due to `RetrievalClientOnly` 71 Code: 4, 72 Msg: &SubscribeMsg{ 73 Stream: stream, 74 History: nil, 75 Priority: Top, 76 }, 77 Peer: node.ID(), 78 }, 79 { //expect a retrieve request message for the given hash 80 Code: 5, 81 Msg: &RetrieveRequestMsg{ 82 Addr: hash0[:], 83 SkipCheck: true, 84 }, 85 Peer: node.ID(), 86 }, 87 }, 88 }) 89 90 if err != nil { 91 t.Fatalf("Expected no error, got %v", err) 92 } 93 } 94 95 //Test requesting a chunk from a peer then issuing a "empty" OfferedHashesMsg (no hashes available yet) 96 //Should time out as the peer does not have the chunk (no syncing happened previously) 97 func TestStreamerUpstreamRetrieveRequestMsgExchangeWithoutStore(t *testing.T) { 98 tester, streamer, _, teardown, err := newStreamerTester(&RegistryOptions{ 99 Retrieval: RetrievalEnabled, 100 Syncing: SyncingDisabled, //do no syncing 101 }) 102 if err != nil { 103 t.Fatal(err) 104 } 105 defer teardown() 106 107 node := tester.Nodes[0] 108 109 chunk := storage.NewChunk(storage.Address(hash0[:]), nil) 110 111 peer := streamer.getPeer(node.ID()) 112 113 stream := NewStream(swarmChunkServerStreamName, "", true) 114 //simulate pre-subscription to RETRIEVE_REQUEST stream on peer 115 peer.handleSubscribeMsg(context.TODO(), &SubscribeMsg{ 116 Stream: stream, 117 History: nil, 118 Priority: Top, 119 }) 120 121 //test the exchange 122 err = tester.TestExchanges(p2ptest.Exchange{ 123 Expects: []p2ptest.Expect{ 124 { //first expect a subscription to the RETRIEVE_REQUEST stream 125 Code: 4, 126 Msg: &SubscribeMsg{ 127 Stream: stream, 128 History: nil, 129 Priority: Top, 130 }, 131 Peer: node.ID(), 132 }, 133 }, 134 }, p2ptest.Exchange{ 135 Label: "RetrieveRequestMsg", 136 Triggers: []p2ptest.Trigger{ 137 { //then the actual RETRIEVE_REQUEST.... 138 Code: 5, 139 Msg: &RetrieveRequestMsg{ 140 Addr: chunk.Address()[:], 141 }, 142 Peer: node.ID(), 143 }, 144 }, 145 Expects: []p2ptest.Expect{ 146 { //to which the peer responds with offered hashes 147 Code: 1, 148 Msg: &OfferedHashesMsg{ 149 HandoverProof: nil, 150 Hashes: nil, 151 From: 0, 152 To: 0, 153 }, 154 Peer: node.ID(), 155 }, 156 }, 157 }) 158 159 //should fail with a timeout as the peer we are requesting 160 //the chunk from does not have the chunk 161 expectedError := `exchange #1 "RetrieveRequestMsg": timed out` 162 if err == nil || err.Error() != expectedError { 163 t.Fatalf("Expected error %v, got %v", expectedError, err) 164 } 165 } 166 167 // upstream request server receives a retrieve Request and responds with 168 // offered hashes or delivery if skipHash is set to true 169 func TestStreamerUpstreamRetrieveRequestMsgExchange(t *testing.T) { 170 tester, streamer, localStore, teardown, err := newStreamerTester(&RegistryOptions{ 171 Retrieval: RetrievalEnabled, 172 Syncing: SyncingDisabled, 173 }) 174 if err != nil { 175 t.Fatal(err) 176 } 177 defer teardown() 178 179 node := tester.Nodes[0] 180 181 peer := streamer.getPeer(node.ID()) 182 183 stream := NewStream(swarmChunkServerStreamName, "", true) 184 185 peer.handleSubscribeMsg(context.TODO(), &SubscribeMsg{ 186 Stream: stream, 187 History: nil, 188 Priority: Top, 189 }) 190 191 hash := storage.Address(hash0[:]) 192 chunk := storage.NewChunk(hash, hash) 193 err = localStore.Put(context.TODO(), chunk) 194 if err != nil { 195 t.Fatalf("Expected no err got %v", err) 196 } 197 198 err = tester.TestExchanges(p2ptest.Exchange{ 199 Expects: []p2ptest.Expect{ 200 { 201 Code: 4, 202 Msg: &SubscribeMsg{ 203 Stream: stream, 204 History: nil, 205 Priority: Top, 206 }, 207 Peer: node.ID(), 208 }, 209 }, 210 }, p2ptest.Exchange{ 211 Label: "RetrieveRequestMsg", 212 Triggers: []p2ptest.Trigger{ 213 { 214 Code: 5, 215 Msg: &RetrieveRequestMsg{ 216 Addr: hash, 217 }, 218 Peer: node.ID(), 219 }, 220 }, 221 Expects: []p2ptest.Expect{ 222 { 223 Code: 1, 224 Msg: &OfferedHashesMsg{ 225 HandoverProof: &HandoverProof{ 226 Handover: &Handover{}, 227 }, 228 Hashes: hash, 229 From: 0, 230 // TODO: why is this 32??? 231 To: 32, 232 Stream: stream, 233 }, 234 Peer: node.ID(), 235 }, 236 }, 237 }) 238 239 if err != nil { 240 t.Fatal(err) 241 } 242 243 hash = storage.Address(hash1[:]) 244 chunk = storage.NewChunk(hash, hash1[:]) 245 err = localStore.Put(context.TODO(), chunk) 246 if err != nil { 247 t.Fatalf("Expected no err got %v", err) 248 } 249 250 err = tester.TestExchanges(p2ptest.Exchange{ 251 Label: "RetrieveRequestMsg", 252 Triggers: []p2ptest.Trigger{ 253 { 254 Code: 5, 255 Msg: &RetrieveRequestMsg{ 256 Addr: hash, 257 SkipCheck: true, 258 }, 259 Peer: node.ID(), 260 }, 261 }, 262 Expects: []p2ptest.Expect{ 263 { 264 Code: 6, 265 Msg: &ChunkDeliveryMsg{ 266 Addr: hash, 267 SData: hash, 268 }, 269 Peer: node.ID(), 270 }, 271 }, 272 }) 273 274 if err != nil { 275 t.Fatal(err) 276 } 277 } 278 279 // if there is one peer in the Kademlia, RequestFromPeers should return it 280 func TestRequestFromPeers(t *testing.T) { 281 dummyPeerID := enode.HexID("3431c3939e1ee2a6345e976a8234f9870152d64879f30bc272a074f6859e75e8") 282 283 addr := network.RandomAddr() 284 to := network.NewKademlia(addr.OAddr, network.NewKadParams()) 285 delivery := NewDelivery(to, nil) 286 protocolsPeer := protocols.NewPeer(p2p.NewPeer(dummyPeerID, "dummy", nil), nil, nil) 287 peer := network.NewPeer(&network.BzzPeer{ 288 BzzAddr: network.RandomAddr(), 289 LightNode: false, 290 Peer: protocolsPeer, 291 }, to) 292 to.On(peer) 293 r := NewRegistry(addr.ID(), delivery, nil, nil, nil, nil) 294 295 // an empty priorityQueue has to be created to prevent a goroutine being called after the test has finished 296 sp := &Peer{ 297 Peer: protocolsPeer, 298 pq: pq.New(int(PriorityQueue), PriorityQueueCap), 299 streamer: r, 300 } 301 r.setPeer(sp) 302 req := network.NewRequest( 303 storage.Address(hash0[:]), 304 true, 305 &sync.Map{}, 306 ) 307 ctx := context.Background() 308 id, _, err := delivery.RequestFromPeers(ctx, req) 309 310 if err != nil { 311 t.Fatal(err) 312 } 313 if *id != dummyPeerID { 314 t.Fatalf("Expected an id, got %v", id) 315 } 316 } 317 318 // RequestFromPeers should not return light nodes 319 func TestRequestFromPeersWithLightNode(t *testing.T) { 320 dummyPeerID := enode.HexID("3431c3939e1ee2a6345e976a8234f9870152d64879f30bc272a074f6859e75e8") 321 322 addr := network.RandomAddr() 323 to := network.NewKademlia(addr.OAddr, network.NewKadParams()) 324 delivery := NewDelivery(to, nil) 325 326 protocolsPeer := protocols.NewPeer(p2p.NewPeer(dummyPeerID, "dummy", nil), nil, nil) 327 // setting up a lightnode 328 peer := network.NewPeer(&network.BzzPeer{ 329 BzzAddr: network.RandomAddr(), 330 LightNode: true, 331 Peer: protocolsPeer, 332 }, to) 333 to.On(peer) 334 r := NewRegistry(addr.ID(), delivery, nil, nil, nil, nil) 335 // an empty priorityQueue has to be created to prevent a goroutine being called after the test has finished 336 sp := &Peer{ 337 Peer: protocolsPeer, 338 pq: pq.New(int(PriorityQueue), PriorityQueueCap), 339 streamer: r, 340 } 341 r.setPeer(sp) 342 343 req := network.NewRequest( 344 storage.Address(hash0[:]), 345 true, 346 &sync.Map{}, 347 ) 348 349 ctx := context.Background() 350 // making a request which should return with "no peer found" 351 _, _, err := delivery.RequestFromPeers(ctx, req) 352 353 expectedError := "no peer found" 354 if err.Error() != expectedError { 355 t.Fatalf("expected '%v', got %v", expectedError, err) 356 } 357 } 358 359 func TestStreamerDownstreamChunkDeliveryMsgExchange(t *testing.T) { 360 tester, streamer, localStore, teardown, err := newStreamerTester(&RegistryOptions{ 361 Retrieval: RetrievalDisabled, 362 Syncing: SyncingDisabled, 363 }) 364 if err != nil { 365 t.Fatal(err) 366 } 367 defer teardown() 368 369 streamer.RegisterClientFunc("foo", func(p *Peer, t string, live bool) (Client, error) { 370 return &testClient{ 371 t: t, 372 }, nil 373 }) 374 375 node := tester.Nodes[0] 376 377 //subscribe to custom stream 378 stream := NewStream("foo", "", true) 379 err = streamer.Subscribe(node.ID(), stream, NewRange(5, 8), Top) 380 if err != nil { 381 t.Fatalf("Expected no error, got %v", err) 382 } 383 384 chunkKey := hash0[:] 385 chunkData := hash1[:] 386 387 err = tester.TestExchanges(p2ptest.Exchange{ 388 Label: "Subscribe message", 389 Expects: []p2ptest.Expect{ 390 { //first expect subscription to the custom stream... 391 Code: 4, 392 Msg: &SubscribeMsg{ 393 Stream: stream, 394 History: NewRange(5, 8), 395 Priority: Top, 396 }, 397 Peer: node.ID(), 398 }, 399 }, 400 }, 401 p2ptest.Exchange{ 402 Label: "ChunkDelivery message", 403 Triggers: []p2ptest.Trigger{ 404 { //...then trigger a chunk delivery for the given chunk from peer in order for 405 //local node to get the chunk delivered 406 Code: 6, 407 Msg: &ChunkDeliveryMsg{ 408 Addr: chunkKey, 409 SData: chunkData, 410 }, 411 Peer: node.ID(), 412 }, 413 }, 414 }) 415 416 if err != nil { 417 t.Fatalf("Expected no error, got %v", err) 418 } 419 ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) 420 defer cancel() 421 422 // wait for the chunk to get stored 423 storedChunk, err := localStore.Get(ctx, chunkKey) 424 for err != nil { 425 select { 426 case <-ctx.Done(): 427 t.Fatalf("Chunk is not in localstore after timeout, err: %v", err) 428 default: 429 } 430 storedChunk, err = localStore.Get(ctx, chunkKey) 431 time.Sleep(50 * time.Millisecond) 432 } 433 434 if err != nil { 435 t.Fatalf("Expected no error, got %v", err) 436 } 437 438 if !bytes.Equal(storedChunk.Data(), chunkData) { 439 t.Fatal("Retrieved chunk has different data than original") 440 } 441 442 } 443 444 func TestDeliveryFromNodes(t *testing.T) { 445 testDeliveryFromNodes(t, 2, dataChunkCount, true) 446 testDeliveryFromNodes(t, 2, dataChunkCount, false) 447 testDeliveryFromNodes(t, 4, dataChunkCount, true) 448 testDeliveryFromNodes(t, 4, dataChunkCount, false) 449 450 if testutil.RaceEnabled { 451 // Travis cannot handle more nodes with -race; would time out. 452 return 453 } 454 455 testDeliveryFromNodes(t, 8, dataChunkCount, true) 456 testDeliveryFromNodes(t, 8, dataChunkCount, false) 457 testDeliveryFromNodes(t, 16, dataChunkCount, true) 458 testDeliveryFromNodes(t, 16, dataChunkCount, false) 459 } 460 461 func testDeliveryFromNodes(t *testing.T, nodes, chunkCount int, skipCheck bool) { 462 t.Helper() 463 t.Run(fmt.Sprintf("testDeliveryFromNodes_%d_%d_skipCheck_%t", nodes, chunkCount, skipCheck), func(t *testing.T) { 464 sim := simulation.New(map[string]simulation.ServiceFunc{ 465 "streamer": func(ctx *adapters.ServiceContext, bucket *sync.Map) (s node.Service, cleanup func(), err error) { 466 addr, netStore, delivery, clean, err := newNetStoreAndDelivery(ctx, bucket) 467 if err != nil { 468 return nil, nil, err 469 } 470 471 r := NewRegistry(addr.ID(), delivery, netStore, state.NewInmemoryStore(), &RegistryOptions{ 472 SkipCheck: skipCheck, 473 Syncing: SyncingDisabled, 474 Retrieval: RetrievalEnabled, 475 }, nil) 476 bucket.Store(bucketKeyRegistry, r) 477 478 cleanup = func() { 479 r.Close() 480 clean() 481 } 482 483 return r, cleanup, nil 484 }, 485 }) 486 defer sim.Close() 487 488 log.Info("Adding nodes to simulation") 489 _, err := sim.AddNodesAndConnectChain(nodes) 490 if err != nil { 491 t.Fatal(err) 492 } 493 494 log.Info("Starting simulation") 495 ctx, cancel := context.WithCancel(context.Background()) 496 defer cancel() 497 result := sim.Run(ctx, func(ctx context.Context, sim *simulation.Simulation) (err error) { 498 nodeIDs := sim.UpNodeIDs() 499 //determine the pivot node to be the first node of the simulation 500 pivot := nodeIDs[0] 501 502 //distribute chunks of a random file into Stores of nodes 1 to nodes 503 //we will do this by creating a file store with an underlying round-robin store: 504 //the file store will create a hash for the uploaded file, but every chunk will be 505 //distributed to different nodes via round-robin scheduling 506 log.Debug("Writing file to round-robin file store") 507 //to do this, we create an array for chunkstores (length minus one, the pivot node) 508 stores := make([]storage.ChunkStore, len(nodeIDs)-1) 509 //we then need to get all stores from the sim.... 510 lStores := sim.NodesItems(bucketKeyStore) 511 i := 0 512 //...iterate the buckets... 513 for id, bucketVal := range lStores { 514 //...and remove the one which is the pivot node 515 if id == pivot { 516 continue 517 } 518 //the other ones are added to the array... 519 stores[i] = bucketVal.(storage.ChunkStore) 520 i++ 521 } 522 //...which then gets passed to the round-robin file store 523 roundRobinFileStore := storage.NewFileStore(newRoundRobinStore(stores...), storage.NewFileStoreParams()) 524 //now we can actually upload a (random) file to the round-robin store 525 size := chunkCount * chunkSize 526 log.Debug("Storing data to file store") 527 fileHash, wait, err := roundRobinFileStore.Store(ctx, testutil.RandomReader(1, size), int64(size), false) 528 // wait until all chunks stored 529 if err != nil { 530 return err 531 } 532 err = wait(ctx) 533 if err != nil { 534 return err 535 } 536 537 //get the pivot node's filestore 538 item, ok := sim.NodeItem(pivot, bucketKeyFileStore) 539 if !ok { 540 return fmt.Errorf("No filestore") 541 } 542 pivotFileStore := item.(*storage.FileStore) 543 log.Debug("Starting retrieval routine") 544 retErrC := make(chan error) 545 go func() { 546 // start the retrieval on the pivot node - this will spawn retrieve requests for missing chunks 547 // we must wait for the peer connections to have started before requesting 548 n, err := readAll(pivotFileStore, fileHash) 549 log.Info(fmt.Sprintf("retrieved %v", fileHash), "read", n, "err", err) 550 retErrC <- err 551 }() 552 553 disconnected := watchDisconnections(ctx, sim) 554 defer func() { 555 if err != nil && disconnected.bool() { 556 err = errors.New("disconnect events received") 557 } 558 }() 559 560 //finally check that the pivot node gets all chunks via the root hash 561 log.Debug("Check retrieval") 562 success := true 563 var total int64 564 total, err = readAll(pivotFileStore, fileHash) 565 if err != nil { 566 return err 567 } 568 log.Info(fmt.Sprintf("check if %08x is available locally: number of bytes read %v/%v (error: %v)", fileHash, total, size, err)) 569 if err != nil || total != int64(size) { 570 success = false 571 } 572 573 if !success { 574 return fmt.Errorf("Test failed, chunks not available on all nodes") 575 } 576 if err := <-retErrC; err != nil { 577 return fmt.Errorf("requesting chunks: %v", err) 578 } 579 log.Debug("Test terminated successfully") 580 return nil 581 }) 582 if result.Error != nil { 583 t.Fatal(result.Error) 584 } 585 }) 586 } 587 588 func BenchmarkDeliveryFromNodesWithoutCheck(b *testing.B) { 589 for chunks := 32; chunks <= 128; chunks *= 2 { 590 for i := 2; i < 32; i *= 2 { 591 b.Run( 592 fmt.Sprintf("nodes=%v,chunks=%v", i, chunks), 593 func(b *testing.B) { 594 benchmarkDeliveryFromNodes(b, i, chunks, true) 595 }, 596 ) 597 } 598 } 599 } 600 601 func BenchmarkDeliveryFromNodesWithCheck(b *testing.B) { 602 for chunks := 32; chunks <= 128; chunks *= 2 { 603 for i := 2; i < 32; i *= 2 { 604 b.Run( 605 fmt.Sprintf("nodes=%v,chunks=%v", i, chunks), 606 func(b *testing.B) { 607 benchmarkDeliveryFromNodes(b, i, chunks, false) 608 }, 609 ) 610 } 611 } 612 } 613 614 func benchmarkDeliveryFromNodes(b *testing.B, nodes, chunkCount int, skipCheck bool) { 615 sim := simulation.New(map[string]simulation.ServiceFunc{ 616 "streamer": func(ctx *adapters.ServiceContext, bucket *sync.Map) (s node.Service, cleanup func(), err error) { 617 addr, netStore, delivery, clean, err := newNetStoreAndDelivery(ctx, bucket) 618 if err != nil { 619 return nil, nil, err 620 } 621 622 r := NewRegistry(addr.ID(), delivery, netStore, state.NewInmemoryStore(), &RegistryOptions{ 623 SkipCheck: skipCheck, 624 Syncing: SyncingDisabled, 625 Retrieval: RetrievalDisabled, 626 SyncUpdateDelay: 0, 627 }, nil) 628 bucket.Store(bucketKeyRegistry, r) 629 630 cleanup = func() { 631 r.Close() 632 clean() 633 } 634 635 return r, cleanup, nil 636 }, 637 }) 638 defer sim.Close() 639 640 log.Info("Initializing test config") 641 _, err := sim.AddNodesAndConnectChain(nodes) 642 if err != nil { 643 b.Fatal(err) 644 } 645 646 ctx, cancel := context.WithCancel(context.Background()) 647 defer cancel() 648 result := sim.Run(ctx, func(ctx context.Context, sim *simulation.Simulation) (err error) { 649 nodeIDs := sim.UpNodeIDs() 650 node := nodeIDs[len(nodeIDs)-1] 651 652 item, ok := sim.NodeItem(node, bucketKeyFileStore) 653 if !ok { 654 return errors.New("No filestore") 655 } 656 remoteFileStore := item.(*storage.FileStore) 657 658 pivotNode := nodeIDs[0] 659 item, ok = sim.NodeItem(pivotNode, bucketKeyNetStore) 660 if !ok { 661 return errors.New("No filestore") 662 } 663 netStore := item.(*storage.NetStore) 664 665 if _, err := sim.WaitTillHealthy(ctx); err != nil { 666 return err 667 } 668 669 disconnected := watchDisconnections(ctx, sim) 670 defer func() { 671 if err != nil && disconnected.bool() { 672 err = errors.New("disconnect events received") 673 } 674 }() 675 // benchmark loop 676 b.ResetTimer() 677 b.StopTimer() 678 Loop: 679 for i := 0; i < b.N; i++ { 680 // uploading chunkCount random chunks to the last node 681 hashes := make([]storage.Address, chunkCount) 682 for i := 0; i < chunkCount; i++ { 683 // create actual size real chunks 684 ctx := context.TODO() 685 hash, wait, err := remoteFileStore.Store(ctx, testutil.RandomReader(i, chunkSize), int64(chunkSize), false) 686 if err != nil { 687 return fmt.Errorf("store: %v", err) 688 } 689 // wait until all chunks stored 690 err = wait(ctx) 691 if err != nil { 692 return fmt.Errorf("wait store: %v", err) 693 } 694 // collect the hashes 695 hashes[i] = hash 696 } 697 // now benchmark the actual retrieval 698 // netstore.Get is called for each hash in a go routine and errors are collected 699 b.StartTimer() 700 errs := make(chan error) 701 for _, hash := range hashes { 702 go func(h storage.Address) { 703 _, err := netStore.Get(ctx, h) 704 log.Warn("test check netstore get", "hash", h, "err", err) 705 errs <- err 706 }(hash) 707 } 708 // count and report retrieval errors 709 // if there are misses then chunk timeout is too low for the distance and volume (?) 710 var total, misses int 711 for err := range errs { 712 if err != nil { 713 log.Warn(err.Error()) 714 misses++ 715 } 716 total++ 717 if total == chunkCount { 718 break 719 } 720 } 721 b.StopTimer() 722 723 if misses > 0 { 724 err = fmt.Errorf("%v chunk not found out of %v", misses, total) 725 break Loop 726 } 727 } 728 return err 729 }) 730 if result.Error != nil { 731 b.Fatal(result.Error) 732 } 733 734 }