diff --git a/lbrynet/dht/contact.py b/lbrynet/dht/contact.py index 6109d9f9a..cba054e0d 100644 --- a/lbrynet/dht/contact.py +++ b/lbrynet/dht/contact.py @@ -1,13 +1,3 @@ -#!/usr/bin/env python -# -# This library is free software, distributed under the terms of -# the GNU Lesser General Public License Version 3, or any later version. -# See the COPYING file included in this archive -# -# The docstrings in this module contain epytext markup; API documentation -# may be created by processing this file with epydoc: http://epydoc.sf.net - - class Contact(object): """ Encapsulation for remote contact diff --git a/lbrynet/dht/datastore.py b/lbrynet/dht/datastore.py index bdaf47644..d67401240 100644 --- a/lbrynet/dht/datastore.py +++ b/lbrynet/dht/datastore.py @@ -1,33 +1,13 @@ -#!/usr/bin/env python -# -# This library is free software, distributed under the terms of -# the GNU Lesser General Public License Version 3, or any later version. -# See the COPYING file included in this archive -# -# The docstrings in this module contain epytext markup; API documentation -# may be created by processing this file with epydoc: http://epydoc.sf.net - import UserDict import time import constants +from interface import IDataStore +from zope.interface import implements -class DataStore(UserDict.DictMixin): - """ Interface for classes implementing physical storage (for data - published via the "STORE" RPC) for the Kademlia DHT - - @note: This provides an interface for a dict-like object - """ - - def keys(self): - """ Return a list of the keys in this data store """ - - def addPeerToBlob(self, key, value, lastPublished, originallyPublished, originalPublisherID): - pass - - -class DictDataStore(DataStore): +class DictDataStore(UserDict.DictMixin): """ A datastore using an in-memory Python dictionary """ + implements(IDataStore) def __init__(self): # Dictionary format: diff --git a/lbrynet/dht/delay.py b/lbrynet/dht/delay.py new file mode 100644 index 000000000..9610a73f8 --- /dev/null +++ b/lbrynet/dht/delay.py @@ -0,0 +1,22 @@ +import time + + +class Delay(object): + maxToSendDelay = 10 ** -3 # 0.05 + minToSendDelay = 10 ** -5 # 0.01 + + def __init__(self, start=0): + self._next = start + + # TODO: explain why this logic is like it is. And add tests that + # show that it actually does what it needs to do. + def __call__(self): + ts = time.time() + delay = 0 + if ts >= self._next: + delay = self.minToSendDelay + self._next = ts + self.minToSendDelay + else: + delay = (self._next - ts) + self.maxToSendDelay + self._next += self.maxToSendDelay + return delay diff --git a/lbrynet/dht/encoding.py b/lbrynet/dht/encoding.py index bc7e88ca0..45aeb3496 100644 --- a/lbrynet/dht/encoding.py +++ b/lbrynet/dht/encoding.py @@ -1,17 +1,4 @@ -#!/usr/bin/env python -# -# This library is free software, distributed under the terms of -# the GNU Lesser General Public License Version 3, or any later version. -# See the COPYING file included in this archive -# -# The docstrings in this module contain epytext markup; API documentation -# may be created by processing this file with epydoc: http://epydoc.sf.net - - -class DecodeError(Exception): - """ Should be raised by an C{Encoding} implementation if decode operation - fails - """ +from error import DecodeError class Encoding(object): diff --git a/lbrynet/dht/error.py b/lbrynet/dht/error.py new file mode 100644 index 000000000..78daee46b --- /dev/null +++ b/lbrynet/dht/error.py @@ -0,0 +1,31 @@ +import binascii + + +class DecodeError(Exception): + """ + Should be raised by an C{Encoding} implementation if decode operation + fails + """ + pass + + +class BucketFull(Exception): + """ + Raised when the bucket is full + """ + pass + + +class UnknownRemoteException(Exception): + pass + + +class TimeoutError(Exception): + """ Raised when a RPC times out """ + + def __init__(self, remote_contact_id): + # remote_contact_id is a binary blob so we need to convert it + # into something more readable + msg = 'Timeout connecting to {}'.format(binascii.hexlify(remote_contact_id)) + Exception.__init__(self, msg) + self.remote_contact_id = remote_contact_id diff --git a/lbrynet/dht/interface.py b/lbrynet/dht/interface.py new file mode 100644 index 000000000..0648d6d21 --- /dev/null +++ b/lbrynet/dht/interface.py @@ -0,0 +1,114 @@ +from zope.interface import Interface + + +class IDataStore(Interface): + """ Interface for classes implementing physical storage (for data + published via the "STORE" RPC) for the Kademlia DHT + + @note: This provides an interface for a dict-like object + """ + + def keys(self): + """ Return a list of the keys in this data store """ + pass + + def removeExpiredPeers(self): + pass + + def hasPeersForBlob(self, key): + pass + + def addPeerToBlob(self, key, value, lastPublished, originallyPublished, originalPublisherID): + pass + + def getPeersForBlob(self, key): + pass + + +class IRoutingTable(Interface): + """ Interface for RPC message translators/formatters + + Classes inheriting from this should provide a suitable routing table for + a parent Node object (i.e. the local entity in the Kademlia network) + """ + + def __init__(self, parentNodeID): + """ + @param parentNodeID: The n-bit node ID of the node to which this + routing table belongs + @type parentNodeID: str + """ + + def addContact(self, contact): + """ Add the given contact to the correct k-bucket; if it already + exists, its status will be updated + + @param contact: The contact to add to this node's k-buckets + @type contact: kademlia.contact.Contact + """ + + def findCloseNodes(self, key, count, _rpcNodeID=None): + """ Finds a number of known nodes closest to the node/value with the + specified key. + + @param key: the n-bit key (i.e. the node or value ID) to search for + @type key: str + @param count: the amount of contacts to return + @type count: int + @param _rpcNodeID: Used during RPC, this is be the sender's Node ID + Whatever ID is passed in the paramater will get + excluded from the list of returned contacts. + @type _rpcNodeID: str + + @return: A list of node contacts (C{kademlia.contact.Contact instances}) + closest to the specified key. + This method will return C{k} (or C{count}, if specified) + contacts if at all possible; it will only return fewer if the + node is returning all of the contacts that it knows of. + @rtype: list + """ + + def getContact(self, contactID): + """ Returns the (known) contact with the specified node ID + + @raise ValueError: No contact with the specified contact ID is known + by this node + """ + + def getRefreshList(self, startIndex=0, force=False): + """ Finds all k-buckets that need refreshing, starting at the + k-bucket with the specified index, and returns IDs to be searched for + in order to refresh those k-buckets + + @param startIndex: The index of the bucket to start refreshing at; + this bucket and those further away from it will + be refreshed. For example, when joining the + network, this node will set this to the index of + the bucket after the one containing it's closest + neighbour. + @type startIndex: index + @param force: If this is C{True}, all buckets (in the specified range) + will be refreshed, regardless of the time they were last + accessed. + @type force: bool + + @return: A list of node ID's that the parent node should search for + in order to refresh the routing Table + @rtype: list + """ + + def removeContact(self, contactID): + """ Remove the contact with the specified node ID from the routing + table + + @param contactID: The node ID of the contact to remove + @type contactID: str + """ + + def touchKBucket(self, key): + """ Update the "last accessed" timestamp of the k-bucket which covers + the range containing the specified key in the key/ID space + + @param key: A key in the range of the target k-bucket + @type key: str + """ diff --git a/lbrynet/dht/kbucket.py b/lbrynet/dht/kbucket.py index 227fec409..ead763895 100644 --- a/lbrynet/dht/kbucket.py +++ b/lbrynet/dht/kbucket.py @@ -1,17 +1,5 @@ -#!/usr/bin/env python -# -# This library is free software, distributed under the terms of -# the GNU Lesser General Public License Version 3, or any later version. -# See the COPYING file included in this archive -# -# The docstrings in this module contain epytext markup; API documentation -# may be created by processing this file with epydoc: http://epydoc.sf.net - import constants - - -class BucketFull(Exception): - """ Raised when the bucket is full """ +from error import BucketFull class KBucket(object): diff --git a/lbrynet/dht/protocol.py b/lbrynet/dht/protocol.py index ba7aba586..17c1f2dd1 100644 --- a/lbrynet/dht/protocol.py +++ b/lbrynet/dht/protocol.py @@ -1,14 +1,4 @@ -#!/usr/bin/env python -# -# This library is free software, distributed under the terms of -# the GNU Lesser General Public License Version 3, or any later version. -# See the COPYING file included in this archive -# -# The docstrings in this module contain epytext markup; API documentation -# may be created by processing this file with epydoc: http://epydoc.sf.net - import logging -import binascii import time import socket import errno @@ -21,42 +11,12 @@ import encoding import msgtypes import msgformat from contact import Contact +from error import UnknownRemoteException, TimeoutError +from delay import Delay log = logging.getLogger(__name__) -class TimeoutError(Exception): - """ Raised when a RPC times out """ - - def __init__(self, remote_contact_id): - # remote_contact_id is a binary blob so we need to convert it - # into something more readable - msg = 'Timeout connecting to {}'.format(binascii.hexlify(remote_contact_id)) - Exception.__init__(self, msg) - self.remote_contact_id = remote_contact_id - - -class Delay(object): - maxToSendDelay = 10 ** -3 # 0.05 - minToSendDelay = 10 ** -5 # 0.01 - - def __init__(self, start=0): - self._next = start - - # TODO: explain why this logic is like it is. And add tests that - # show that it actually does what it needs to do. - def __call__(self): - ts = time.time() - delay = 0 - if ts >= self._next: - delay = self.minToSendDelay - self._next = ts + self.minToSendDelay - else: - delay = (self._next - ts) + self.maxToSendDelay - self._next += self.maxToSendDelay - return delay - - class KademliaProtocol(protocol.DatagramProtocol): """ Implements all low-level network-related functions of a Kademlia node """ diff --git a/lbrynet/dht/routingtable.py b/lbrynet/dht/routingtable.py index 1f6cca926..c03dd0fd0 100644 --- a/lbrynet/dht/routingtable.py +++ b/lbrynet/dht/routingtable.py @@ -7,102 +7,17 @@ import time import random +from zope.interface import implements import constants import kbucket +from interface import IRoutingTable +from error import TimeoutError +import logging -from protocol import TimeoutError +log = logging.getLogger(__name__) -class RoutingTable(object): - """ Interface for RPC message translators/formatters - - Classes inheriting from this should provide a suitable routing table for - a parent Node object (i.e. the local entity in the Kademlia network) - """ - - def __init__(self, parentNodeID): - """ - @param parentNodeID: The n-bit node ID of the node to which this - routing table belongs - @type parentNodeID: str - """ - - def addContact(self, contact): - """ Add the given contact to the correct k-bucket; if it already - exists, its status will be updated - - @param contact: The contact to add to this node's k-buckets - @type contact: kademlia.contact.Contact - """ - - def findCloseNodes(self, key, count, _rpcNodeID=None): - """ Finds a number of known nodes closest to the node/value with the - specified key. - - @param key: the n-bit key (i.e. the node or value ID) to search for - @type key: str - @param count: the amount of contacts to return - @type count: int - @param _rpcNodeID: Used during RPC, this is be the sender's Node ID - Whatever ID is passed in the paramater will get - excluded from the list of returned contacts. - @type _rpcNodeID: str - - @return: A list of node contacts (C{kademlia.contact.Contact instances}) - closest to the specified key. - This method will return C{k} (or C{count}, if specified) - contacts if at all possible; it will only return fewer if the - node is returning all of the contacts that it knows of. - @rtype: list - """ - - def getContact(self, contactID): - """ Returns the (known) contact with the specified node ID - - @raise ValueError: No contact with the specified contact ID is known - by this node - """ - - def getRefreshList(self, startIndex=0, force=False): - """ Finds all k-buckets that need refreshing, starting at the - k-bucket with the specified index, and returns IDs to be searched for - in order to refresh those k-buckets - - @param startIndex: The index of the bucket to start refreshing at; - this bucket and those further away from it will - be refreshed. For example, when joining the - network, this node will set this to the index of - the bucket after the one containing it's closest - neighbour. - @type startIndex: index - @param force: If this is C{True}, all buckets (in the specified range) - will be refreshed, regardless of the time they were last - accessed. - @type force: bool - - @return: A list of node ID's that the parent node should search for - in order to refresh the routing Table - @rtype: list - """ - - def removeContact(self, contactID): - """ Remove the contact with the specified node ID from the routing - table - - @param contactID: The node ID of the contact to remove - @type contactID: str - """ - - def touchKBucket(self, key): - """ Update the "last accessed" timestamp of the k-bucket which covers - the range containing the specified key in the key/ID space - - @param key: A key in the range of the target k-bucket - @type key: str - """ - - -class TreeRoutingTable(RoutingTable): +class TreeRoutingTable(object): """ This class implements a routing table used by a Node class. The Kademlia routing table is a binary tree whose leaves are k-buckets, @@ -117,6 +32,7 @@ class TreeRoutingTable(RoutingTable): C{PING} RPC-based k-bucket eviction algorithm described in section 2.2 of that paper. """ + implements(IRoutingTable) def __init__(self, parentNodeID): """