Преглед на файлове

retry block downloads when no response for some time

Malte Kraus преди 8 години
родител
ревизия
80c18aee99
променени са 1 файла, в които са добавени 95 реда и са изтрити 28 реда
  1. 95 28
      src/chainbuilder.py

+ 95 - 28
src/chainbuilder.py

@@ -6,18 +6,63 @@ candidate for an even longer chain that it attempts to download and verify.
 import threading
 import logging
 from typing import List, Dict, Callable, Optional
-from datetime import datetime
+from datetime import datetime, timedelta
 
 from .block import GENESIS_BLOCK, GENESIS_BLOCK_HASH, Block
 from .blockchain import Blockchain
 
 __all__ = ['ChainBuilder']
 
-class PartialChain:
+class BlockRequest:
+    """
+    Stores information about a pending block request and the partial chains that depend on it.
+
+    :ivar partial_chains: The partial chains that wait for this request. These partial chains are
+                          ordered by age, youngest first.
+    :vartype partial_chains: List[List[Block]]
+    :ivar _last_update: The time and date of the last block request to our peers.
+    :vartype _last_update: datetime
+    :ivar _request_count: The number of requests to our peers we have sent.
+    :vartype _request_count: int
+    """
+
+    BLOCK_REQUEST_RETRY_INTERVAL = timedelta(minutes=1)
+    """ The approximate interval after which a block request will be retried. """
+    BLOCK_REQUEST_RETRY_COUNT = 3
+    """ The number of failed requests of a block until we give up and delete the depending partial chains. """
+
+
     def __init__(self):
-        self.blocks = []
-        self.last_update = datetime.utcnow()
-        # TODO: delete partial chains after some time
+        self.partial_chains = [[]]
+        self.clear()
+
+    def clear(self):
+        """ Clears the download count and last update time of this request. """
+        self._last_update = datetime(1970, 1, 1)
+        self._request_count = 0
+
+    def send_request(self, protocol: 'Protocol'):
+        """ Sends a request for the next required block to the given `protocol`. """
+        self._request_count += 1
+        self._last_update = datetime.utcnow()
+        protocol.send_block_request(self.partial_chains[0][-1].prev_block_hash)
+        logging.debug("asking for another block %d (attempt %d)", max(len(r) for r in self.partial_chains), self._request_count)
+
+    def timeout_reached(self) -> bool:
+        """ Returns a bool indicating whether all attempts to download this block have failed. """
+        return self._request_count > self.BLOCK_REQUEST_RETRY_COUNT
+
+    def checked_retry(self, protocol: 'Protocol'):
+        """
+        Retries sending this request, if no response was received for a certain time or if no
+        request was sent yet.
+        """
+
+        if self._last_update + self.BLOCK_REQUEST_RETRY_INTERVAL < datetime.utcnow():
+            if self._request_count >= self.BLOCK_REQUEST_RETRY_COUNT:
+                self._request_count += 1
+            else:
+                self.send_request(protocol)
 
 class ChainBuilder:
     """
@@ -27,7 +72,7 @@ class ChainBuilder:
     :ivar primary_block_chain: The longest fully validated block chain we know of.
     :vartype primary_block_chain: Blockchain
     :ivar _block_requests: A dict from block hashes to lists of partial chains waiting for that block.
-    :vartype _block_requests: Dict[bytes, List[PartialChain]]
+    :vartype _block_requests: Dict[bytes, BlockRequest]
     :ivar block_cache: A cache of received blocks, not bound to any one specific block chain.
     :vartype block_cache: Dict[bytes, Block]
     :ivar unconfirmed_transactions: Known transactions that are not part of the primary block chain.
@@ -39,7 +84,7 @@ class ChainBuilder:
     :vartype protocol: Protocol
     """
 
-    def __init__(self, protocol):
+    def __init__(self, protocol: 'Protocol'):
         self.primary_block_chain = Blockchain()
         self._block_requests = {}
         # TODO: delete some old checkpoints
@@ -94,20 +139,37 @@ class ChainBuilder:
             handler()
 
         self._blockchain_checkpoints[chain.head.hash] = chain
+        self._retry_expired_requests()
+        self._clean_block_requests()
+
+        self.protocol.broadcast_primary_block(chain.head)
 
-        # stop trying to build shorter block chains
+    def _retry_expired_requests(self):
+        """ Sends new block requests to our peers for unanswered pending requests. """
+        for request in self._block_requests.values():
+            request.checked_retry(self.protocol)
+
+    def _clean_block_requests(self):
+        """
+        Deletes partial chains and block requests when they are shorter than the primary block
+        chain or all download attempts failed.
+        """
+        # TODO: call this regularly when not mining
         block_requests = {}
-        for block_hash, requests in self._block_requests.items():
+        for block_hash, request in self._block_requests.items():
+            if request.timeout_reached():
+                logging.info("giving up on a block")
+                continue
+
             new_requests = []
-            for partial_chain in requests:
-                if partial_chain.blocks[0].height > chain.head.height:
+            for partial_chain in request.partial_chains:
+                if partial_chain[0].height > self.primary_block_chain.head.height:
                     new_requests.append(partial_chain)
             if new_requests:
-                block_requests[block_hash] = new_requests
+                request.partial_chains = new_requests
+                block_requests[block_hash] = request
         self._block_requests = block_requests
 
-        self.protocol.broadcast_primary_block(chain.head)
-
     def new_block_received(self, block: 'Block'):
         """ Event handler that is called by the network layer when a block is received. """
         self._assert_thread_safety()
@@ -116,42 +178,47 @@ class ChainBuilder:
             return
         self.block_cache[block.hash] = block
 
+        self._retry_expired_requests()
+
         if block.hash not in self._block_requests:
             if block.height > self.primary_block_chain.head.height:
-                self._block_requests.setdefault(block.hash, []).append(PartialChain())
+                if block.hash not in self._block_requests:
+                    self._block_requests[block.hash] = BlockRequest()
             else:
                 return
 
-        requests = self._block_requests[block.hash]
+        request = self._block_requests[block.hash]
         del self._block_requests[block.hash]
         while True:
-            for partial_chain in requests:
-                partial_chain.blocks.append(block)
-                partial_chain.last_update = datetime.utcnow()
+            for partial_chain in request.partial_chains:
+                partial_chain.append(block)
             if block.prev_block_hash not in self.block_cache or block.prev_block_hash in self._blockchain_checkpoints:
                 break
             block = self.block_cache[block.prev_block_hash]
-        self._block_requests.setdefault(block.prev_block_hash, []).extend(requests)
+        if block.prev_block_hash in self._block_requests:
+            chains = request.partial_chains
+            request = self._block_requests[block.prev_block_hash]
+            request.partial_chains.extend(chains)
+        else:
+            request.clear()
+            self._block_requests[block.prev_block_hash] = request
 
         if block.prev_block_hash in self._blockchain_checkpoints:
             winner = self.primary_block_chain
-            for partial_chain in requests:
+            for partial_chain in request.partial_chains:
                 chain = self._blockchain_checkpoints[block.prev_block_hash]
-                for b in partial_chain.blocks[::-1]:
+                for b in partial_chain[::-1]:
                     next_chain = chain.try_append(b)
                     if next_chain is None:
+                        logging.warning("invalid block")
                         break
                     chain = next_chain
                 if chain.head.height > winner.head.height:
                     winner = chain
+            del self._block_requests[block.prev_block_hash]
             if winner is not self.primary_block_chain:
                 self._new_primary_block_chain(winner)
-        else:
-            # TODO: only do this if we have no pending requests for this block
-            self.protocol.send_block_request(block.prev_block_hash)
-            logging.debug("asking for another block %d", max(len(r.blocks) for r in requests))
-            self._block_requests[block.prev_block_hash] = requests
-
+        request.checked_retry(self.protocol)
 
 from .protocol import Protocol
 from .block import Block