Improved timeout calculation and handling.

This commit is contained in:
Mark Qvist 2021-08-28 20:01:01 +02:00
parent 6d441dac02
commit 2678aeb6a1
5 changed files with 112 additions and 41 deletions

View File

@ -22,7 +22,7 @@ class Destination:
encrypted communication with it. encrypted communication with it.
:param identity: An instance of :ref:`RNS.Identity<api-identity>`. Can hold only public keys for an outgoing destination, or holding private keys for an ingoing. :param identity: An instance of :ref:`RNS.Identity<api-identity>`. Can hold only public keys for an outgoing destination, or holding private keys for an ingoing.
:param direction: ``RNS.Destination.IN`` or ``RNS.Destination.OUT`` :param direction: ``RNS.Destination.IN`` or ``RNS.Destination.OUT``.
:param type: ``RNS.Destination.SINGLE``, ``RNS.Destination.GROUP`` or ``RNS.Destination.PLAIN``. :param type: ``RNS.Destination.SINGLE``, ``RNS.Destination.GROUP`` or ``RNS.Destination.PLAIN``.
:param app_name: A string specifying the app name. :param app_name: A string specifying the app name.
:param \*aspects: Any non-zero number of string arguments. :param \*aspects: Any non-zero number of string arguments.

View File

@ -46,14 +46,15 @@ class Link:
MDU = math.floor((RNS.Reticulum.MDU-RNS.Identity.AES_HMAC_OVERHEAD)/RNS.Identity.AES128_BLOCKSIZE)*RNS.Identity.AES128_BLOCKSIZE - 1 MDU = math.floor((RNS.Reticulum.MDU-RNS.Identity.AES_HMAC_OVERHEAD)/RNS.Identity.AES128_BLOCKSIZE)*RNS.Identity.AES128_BLOCKSIZE - 1
# TODO: This should not be hardcoded, # This value is set at a reasonable
# but calculated from something like # level for a 1 Kb/s channel.
# first-hop RTT latency and distance ESTABLISHMENT_TIMEOUT_PER_HOP = 3
DEFAULT_TIMEOUT = 60.0
""" """
Default timeout for link establishment in seconds. Default timeout for link establishment in seconds per hop to destination.
""" """
TIMEOUT_FACTOR = 3
TRAFFIC_TIMEOUT_FACTOR = 20
KEEPALIVE_TIMEOUT_FACTOR = 4
STALE_GRACE = 2 STALE_GRACE = 2
KEEPALIVE = 360 KEEPALIVE = 360
""" """
@ -119,9 +120,10 @@ class Link:
self.rx = 0 self.rx = 0
self.txbytes = 0 self.txbytes = 0
self.rxbytes = 0 self.rxbytes = 0
self.default_timeout = Link.DEFAULT_TIMEOUT self.establishment_timeout = Link.ESTABLISHMENT_TIMEOUT_PER_HOP * max(1, RNS.Transport.hops_to(destination.hash))
self.proof_timeout = self.default_timeout RNS.log("Establishment timeout set to: "+str(self.establishment_timeout))
self.timeout_factor = Link.TIMEOUT_FACTOR self.traffic_timeout_factor = Link.TRAFFIC_TIMEOUT_FACTOR
self.keepalive_timeout_factor = Link.KEEPALIVE_TIMEOUT_FACTOR
self.keepalive = Link.KEEPALIVE self.keepalive = Link.KEEPALIVE
self.watchdog_lock = False self.watchdog_lock = False
self.status = Link.PENDING self.status = Link.PENDING
@ -273,31 +275,35 @@ class Link:
self.had_outbound() self.had_outbound()
def request(self, path, data = None, response_callback = None, failed_callback = None, timeout = None): def request(self, path, data = None, response_callback = None, failed_callback = None, progress_callback = None, timeout = None):
""" """
Sends a request to the remote peer. Sends a request to the remote peer.
:param path: The request path. :param path: The request path.
:param response_callback: An optional function or method with the signature *response_callback(request_receipt)* to be called when a response is received. See the :ref:`Request Example<example-request>` for more info. :param response_callback: An optional function or method with the signature *response_callback(request_receipt)* to be called when a response is received. See the :ref:`Request Example<example-request>` for more info.
:param failed_callback: An optional function or method with the signature *failed_callback(request_receipt)* to be called when a request fails. See the :ref:`Request Example<example-request>` for more info. :param failed_callback: An optional function or method with the signature *failed_callback(request_receipt)* to be called when a request fails. See the :ref:`Request Example<example-request>` for more info.
:param timeout: An optional timeout in seconds for the request. If *None* is supplied, this defaults to ``RNS.Packet.TIMEOUT``. :param progress_callback: An optional function or method with the signature *progress_callback(request_receipt)* to be called when progress is made receiving the response. Progress can be accessed as a float between 0.0 and 1.0 by the *request_receipt.progress* property.
:param timeout: An optional timeout in seconds for the request. If *None* is supplied it will be calculated based on link RTT.
""" """
request_path_hash = RNS.Identity.truncated_hash(path.encode("utf-8")) request_path_hash = RNS.Identity.truncated_hash(path.encode("utf-8"))
unpacked_request = [time.time(), request_path_hash, data] unpacked_request = [time.time(), request_path_hash, data]
packed_request = umsgpack.packb(unpacked_request) packed_request = umsgpack.packb(unpacked_request)
if timeout == None:
timeout = self.rtt * self.traffic_timeout_factor
if len(packed_request) <= Link.MDU: if len(packed_request) <= Link.MDU:
request_packet = RNS.Packet(self, packed_request, RNS.Packet.DATA, context = RNS.Packet.REQUEST) request_packet = RNS.Packet(self, packed_request, RNS.Packet.DATA, context = RNS.Packet.REQUEST)
packet_receipt = request_packet.send() packet_receipt = request_packet.send()
if timeout != None: packet_receipt.set_timeout(timeout)
packet_receipt.set_timeout(timeout)
return RequestReceipt( return RequestReceipt(
self, self,
packet_receipt = packet_receipt, packet_receipt = packet_receipt,
response_callback = response_callback, response_callback = response_callback,
failed_callback = failed_callback, failed_callback = failed_callback,
progress_callback = progress_callback,
timeout = timeout timeout = timeout
) )
@ -311,6 +317,7 @@ class Link:
resource = request_resource, resource = request_resource,
response_callback = response_callback, response_callback = response_callback,
failed_callback = failed_callback, failed_callback = failed_callback,
progress_callback = progress_callback,
timeout = timeout timeout = timeout
) )
@ -425,9 +432,9 @@ class Link:
# Link was initiated, but no response # Link was initiated, but no response
# from destination yet # from destination yet
if self.status == Link.PENDING: if self.status == Link.PENDING:
next_check = self.request_time + self.proof_timeout next_check = self.request_time + self.establishment_timeout
sleep_time = next_check - time.time() sleep_time = next_check - time.time()
if time.time() >= self.request_time + self.proof_timeout: if time.time() >= self.request_time + self.establishment_timeout:
RNS.log("Link establishment timed out", RNS.LOG_VERBOSE) RNS.log("Link establishment timed out", RNS.LOG_VERBOSE)
self.status = Link.CLOSED self.status = Link.CLOSED
self.teardown_reason = Link.TIMEOUT self.teardown_reason = Link.TIMEOUT
@ -435,9 +442,9 @@ class Link:
sleep_time = 0.001 sleep_time = 0.001
elif self.status == Link.HANDSHAKE: elif self.status == Link.HANDSHAKE:
next_check = self.request_time + self.proof_timeout next_check = self.request_time + self.establishment_timeout
sleep_time = next_check - time.time() sleep_time = next_check - time.time()
if time.time() >= self.request_time + self.proof_timeout: if time.time() >= self.request_time + self.establishment_timeout:
RNS.log("Timeout waiting for RTT packet from link initiator", RNS.LOG_DEBUG) RNS.log("Timeout waiting for RTT packet from link initiator", RNS.LOG_DEBUG)
self.status = Link.CLOSED self.status = Link.CLOSED
self.teardown_reason = Link.TIMEOUT self.teardown_reason = Link.TIMEOUT
@ -446,7 +453,7 @@ class Link:
elif self.status == Link.ACTIVE: elif self.status == Link.ACTIVE:
if time.time() >= self.last_inbound + self.keepalive: if time.time() >= self.last_inbound + self.keepalive:
sleep_time = self.rtt * self.timeout_factor + Link.STALE_GRACE sleep_time = self.rtt * self.keepalive_timeout_factor + Link.STALE_GRACE
self.status = Link.STALE self.status = Link.STALE
if self.initiator: if self.initiator:
self.send_keepalive() self.send_keepalive()
@ -620,7 +627,13 @@ class Link:
if RNS.ResourceAdvertisement.is_request(packet): if RNS.ResourceAdvertisement.is_request(packet):
RNS.Resource.accept(packet, callback=self.request_resource_concluded) RNS.Resource.accept(packet, callback=self.request_resource_concluded)
elif RNS.ResourceAdvertisement.is_response(packet): elif RNS.ResourceAdvertisement.is_response(packet):
RNS.Resource.accept(packet, callback=self.response_resource_concluded) request_id = RNS.ResourceAdvertisement.get_request_id(packet)
for pending_request in self.pending_requests:
if pending_request.request_id == request_id:
RNS.Resource.accept(packet, callback=self.response_resource_concluded, progress_callback=pending_request.response_resource_progress)
pending_request.response_size = RNS.ResourceAdvertisement.get_size(packet)
pending_request.response_transfer_size = RNS.ResourceAdvertisement.get_transfer_size(packet)
pending_request.started_at = time.time()
elif self.resource_strategy == Link.ACCEPT_NONE: elif self.resource_strategy == Link.ACCEPT_NONE:
pass pass
elif self.resource_strategy == Link.ACCEPT_APP: elif self.resource_strategy == Link.ACCEPT_APP:
@ -846,34 +859,41 @@ class RequestReceipt():
DELIVERED = 0x02 DELIVERED = 0x02
READY = 0x03 READY = 0x03
def __init__(self, link, packet_receipt = None, resource = None, response_callback = None, failed_callback = None, timeout = None): def __init__(self, link, packet_receipt = None, resource = None, response_callback = None, failed_callback = None, progress_callback = None, timeout = None):
self.packet_receipt = packet_receipt self.packet_receipt = packet_receipt
self.resource = resource self.resource = resource
self.started_at = None
if self.packet_receipt != None: if self.packet_receipt != None:
self.hash = packet_receipt.truncated_hash self.hash = packet_receipt.truncated_hash
self.packet_receipt.set_timeout_callback(self.request_timed_out) self.packet_receipt.set_timeout_callback(self.request_timed_out)
self.started_at = time.time()
elif self.resource != None: elif self.resource != None:
self.hash = resource.request_id self.hash = resource.request_id
resource.set_callback(self.request_resource_concluded) resource.set_callback(self.request_resource_concluded)
self.link = link self.link = link
self.request_id = self.hash self.request_id = self.hash
self.response = None self.response = None
self.status = RequestReceipt.SENT self.response_transfer_size = None
self.sent_at = time.time() self.response_size = None
self.concluded_at = None self.status = RequestReceipt.SENT
self.sent_at = time.time()
self.progress = 0
self.concluded_at = None
self.response_concluded_at = None
if timeout != None: if timeout != None:
self.timeout = timeout self.timeout = timeout
else: else:
self.timeout = RNS.Packet.TIMEOUT raise ValueError("No timeout specified for request receipt")
self.callbacks = RequestReceiptCallbacks() self.callbacks = RequestReceiptCallbacks()
self.callbacks.response = response_callback self.callbacks.response = response_callback
self.callbacks.failed = failed_callback self.callbacks.failed = failed_callback
self.callbacks.progress = progress_callback
self.link.pending_requests.append(self) self.link.pending_requests.append(self)
@ -881,6 +901,7 @@ class RequestReceipt():
def request_resource_concluded(self, resource): def request_resource_concluded(self, resource):
if resource.status == RNS.Resource.COMPLETE: if resource.status == RNS.Resource.COMPLETE:
RNS.log("Request "+RNS.prettyhexrep(self.request_id)+" successfully sent as resource.", RNS.LOG_DEBUG) RNS.log("Request "+RNS.prettyhexrep(self.request_id)+" successfully sent as resource.", RNS.LOG_DEBUG)
self.started_at = time.time()
self.status = RequestReceipt.DELIVERED self.status = RequestReceipt.DELIVERED
self.__resource_response_timeout = time.time()+self.timeout self.__resource_response_timeout = time.time()+self.timeout
load_thread = threading.Thread(target=self.__resource_response_timeout_job) load_thread = threading.Thread(target=self.__resource_response_timeout_job)
@ -904,6 +925,12 @@ class RequestReceipt():
if self.callbacks.failed != None: if self.callbacks.failed != None:
self.callbacks.failed(self) self.callbacks.failed(self)
def response_resource_progress(self, resource):
self.progress = resource.progress()
self.__resource_response_timeout = time.time()+self.timeout
if self.callbacks.progress != None:
self.callbacks.progress(self)
def __resource_response_timeout_job(self): def __resource_response_timeout_job(self):
while self.status == RequestReceipt.DELIVERED: while self.status == RequestReceipt.DELIVERED:
@ -914,8 +941,14 @@ class RequestReceipt():
def response_received(self, response): def response_received(self, response):
self.progress = 1.0
self.response = response self.response = response
self.status = RequestReceipt.READY self.status = RequestReceipt.READY
self.response_concluded_at = time.time()
if len(response) <= Link.MDU:
self.response_size = len(response)
self.response_transfer_size = len(response)
if self.packet_receipt != None: if self.packet_receipt != None:
self.packet_receipt.status = RNS.PacketReceipt.DELIVERED self.packet_receipt.status = RNS.PacketReceipt.DELIVERED
@ -924,11 +957,20 @@ class RequestReceipt():
if self.packet_receipt.callbacks.delivery != None: if self.packet_receipt.callbacks.delivery != None:
self.packet_receipt.callbacks.delivery(self) self.packet_receipt.callbacks.delivery(self)
if self.callbacks.progress != None:
self.callbacks.progress(self)
if self.callbacks.response != None: if self.callbacks.response != None:
self.callbacks.response(self) self.callbacks.response(self)
def response_time(self):
if self.status == RequestReceipt.READY:
return self.response_concluded_at - self.started_at
class RequestReceiptCallbacks: class RequestReceiptCallbacks:
def __init__(self): def __init__(self):
self.response = None self.response = None
self.failed = None self.failed = None
self.progress = None

View File

@ -71,19 +71,18 @@ class Packet:
# With an MTU of 500, the maximum of data we can # With an MTU of 500, the maximum of data we can
# send in a single encrypted packet is given by # send in a single encrypted packet is given by
# the below calculation; 383 bytes. # the below calculation; 383 bytes.
ENCRYPTED_MDU = math.floor((RNS.Reticulum.MDU-RNS.Identity.AES_HMAC_OVERHEAD-RNS.Identity.KEYSIZE//16)/RNS.Identity.AES128_BLOCKSIZE)*RNS.Identity.AES128_BLOCKSIZE - 1 ENCRYPTED_MDU = math.floor((RNS.Reticulum.MDU-RNS.Identity.AES_HMAC_OVERHEAD-RNS.Identity.KEYSIZE//16)/RNS.Identity.AES128_BLOCKSIZE)*RNS.Identity.AES128_BLOCKSIZE - 1
""" """
The maximum size of the payload data in a single encrypted packet The maximum size of the payload data in a single encrypted packet
""" """
PLAIN_MDU = MDU PLAIN_MDU = MDU
""" """
The maximum size of the payload data in a single unencrypted packet The maximum size of the payload data in a single unencrypted packet
""" """
# TODO: This should be calculated # This value is set at a reasonable
# more intelligently # level for a 1 Kb/s channel.
# Default packet timeout TIMEOUT_PER_HOP = 5
TIMEOUT = 60
def __init__(self, destination, data, packet_type = DATA, context = NONE, transport_type = RNS.Transport.BROADCAST, header_type = HEADER_1, transport_id = None, attached_interface = None, create_receipt = True): def __init__(self, destination, data, packet_type = DATA, context = NONE, transport_type = RNS.Transport.BROADCAST, header_type = HEADER_1, transport_id = None, attached_interface = None, create_receipt = True):
if destination != None: if destination != None:
@ -329,13 +328,18 @@ class PacketReceipt:
self.truncated_hash = packet.getTruncatedHash() self.truncated_hash = packet.getTruncatedHash()
self.sent = True self.sent = True
self.sent_at = time.time() self.sent_at = time.time()
self.timeout = Packet.TIMEOUT
self.proved = False self.proved = False
self.status = PacketReceipt.SENT self.status = PacketReceipt.SENT
self.destination = packet.destination self.destination = packet.destination
self.callbacks = PacketReceiptCallbacks() self.callbacks = PacketReceiptCallbacks()
self.concluded_at = None self.concluded_at = None
if packet.destination.type == RNS.Destination.LINK:
self.timeout = packet.destination.rtt * packet.destination.traffic_timeout_factor
else:
self.timeout = Packet.TIMEOUT_PER_HOP * RNS.Transport.hops_to(self.destination.hash)
def get_status(self): def get_status(self):
""" """
:returns: The status of the associated :ref:`RNS.Packet<api-packet>` instance. Can be one of ``RNS.PacketReceipt.SENT``, ``RNS.PacketReceipt.DELIVERED``, ``RNS.PacketReceipt.FAILED`` or ``RNS.PacketReceipt.CULLED``. :returns: The status of the associated :ref:`RNS.Packet<api-packet>` instance. Can be one of ``RNS.PacketReceipt.SENT``, ``RNS.PacketReceipt.DELIVERED``, ``RNS.PacketReceipt.FAILED`` or ``RNS.PacketReceipt.CULLED``.
@ -448,7 +452,6 @@ class PacketReceipt:
thread = threading.Thread(target=self.callbacks.timeout, args=(self,)) thread = threading.Thread(target=self.callbacks.timeout, args=(self,))
thread.setDaemon(True) thread.setDaemon(True)
thread.start() thread.start()
#self.callbacks.timeout(self)
def set_timeout(self, timeout): def set_timeout(self, timeout):

View File

@ -182,7 +182,7 @@ class Resource:
self.link = link self.link = link
self.max_retries = Resource.MAX_RETRIES self.max_retries = Resource.MAX_RETRIES
self.retries_left = self.max_retries self.retries_left = self.max_retries
self.timeout_factor = self.link.timeout_factor self.timeout_factor = self.link.traffic_timeout_factor
self.sender_grace_time = Resource.SENDER_GRACE_TIME self.sender_grace_time = Resource.SENDER_GRACE_TIME
self.hmu_retry_ok = False self.hmu_retry_ok = False
self.watchdog_lock = False self.watchdog_lock = False
@ -197,7 +197,7 @@ class Resource:
if timeout != None: if timeout != None:
self.timeout = timeout self.timeout = timeout
else: else:
self.timeout = self.link.rtt * 20 self.timeout = self.link.rtt * self.link.traffic_timeout_factor
if data != None: if data != None:
self.initiator = True self.initiator = True
@ -807,6 +807,18 @@ class ResourceAdvertisement:
return adv.q return adv.q
@staticmethod
def get_transfer_size(advertisement_packet):
adv = ResourceAdvertisement.unpack(advertisement_packet.plaintext)
return adv.t
@staticmethod
def get_size(advertisement_packet):
adv = ResourceAdvertisement.unpack(advertisement_packet.plaintext)
return adv.d
def __init__(self, resource=None, request_id=None, is_response=False): def __init__(self, resource=None, request_id=None, is_response=False):
if resource != None: if resource != None:
self.t = resource.size # Transfer size self.t = resource.size # Transfer size

View File

@ -22,7 +22,10 @@ class Transport:
APP_NAME = "rnstransport" APP_NAME = "rnstransport"
PATHFINDER_M = 18 # Max hops PATHFINDER_M = 128 # Max hops
"""
Maximum amount of hops that Reticulum will transport a packet.
"""
PATHFINDER_C = 2.0 # Decay constant PATHFINDER_C = 2.0 # Decay constant
PATHFINDER_R = 1 # Retransmit retries PATHFINDER_R = 1 # Retransmit retries
PATHFINDER_T = 10 # Retry grace period PATHFINDER_T = 10 # Retry grace period
@ -1025,6 +1028,17 @@ class Transport:
else: else:
return False return False
@staticmethod
def hops_to(destination_hash):
"""
:param destination_hash: A destination hash as *bytes*.
:returns: The number of hops to the specified destination, or ``RNS.Transport.PATHFINDER_M`` if the number of hops is unknown.
"""
if destination_hash in Transport.destination_table:
return Transport.destination_table[destination_hash][2]
else:
return Transport.PATHFINDER_M
@staticmethod @staticmethod
def request_path(destination_hash): def request_path(destination_hash):
""" """