diff --git a/aiocoap/blockwise.py b/aiocoap/blockwise.py index 1c368cb2..cf312f37 100644 --- a/aiocoap/blockwise.py +++ b/aiocoap/blockwise.py @@ -52,7 +52,7 @@ class IncompleteException(ConstructionRenderableError): class Block1Spool: def __init__(self): # FIXME: introduce an actual parameter here - self._assemblies = TimeoutDict(numbers.MAX_TRANSMIT_WAIT) + self._assemblies = TimeoutDict(numbers.TransportTuning().MAX_TRANSMIT_WAIT) def feed_and_take(self, req: Message) -> Message: """Assemble the request into the spool. This either produces a @@ -92,7 +92,7 @@ class Block2Cache: """ def __init__(self): # FIXME: introduce an actual parameter here - self._completes = TimeoutDict(numbers.MAX_TRANSMIT_WAIT) + self._completes = TimeoutDict(numbers.TransportTuning().MAX_TRANSMIT_WAIT) async def extract_or_insert(self, req: Message, response_builder: types.CoroutineType): """Given a request message, diff --git a/aiocoap/interfaces.py b/aiocoap/interfaces.py index 3a02dda7..511008eb 100644 --- a/aiocoap/interfaces.py +++ b/aiocoap/interfaces.py @@ -20,8 +20,8 @@ import asyncio import warnings -from aiocoap.numbers.constants import DEFAULT_BLOCK_SIZE_EXP from aiocoap.pipe import Pipe +from aiocoap.numbers.constants import MAX_REGULAR_BLOCK_SIZE_EXP from typing import Optional, Callable @@ -147,7 +147,7 @@ def scheme(Self): communication. (Should there ever be a scheme that addresses the participants differently, a scheme_local will be added.)""" - maximum_block_size_exp = DEFAULT_BLOCK_SIZE_EXP + maximum_block_size_exp = MAX_REGULAR_BLOCK_SIZE_EXP """The maximum negotiated block size that can be sent to this remote.""" # Giving some slack so that barely-larger messages (like OSCORE typically diff --git a/aiocoap/message.py b/aiocoap/message.py index 62715afa..d8b77f68 100644 --- a/aiocoap/message.py +++ b/aiocoap/message.py @@ -17,7 +17,7 @@ from . import error, optiontypes from .numbers.codes import Code, CHANGED from .numbers.types import Type -from .numbers.constants import DEFAULT_BLOCK_SIZE_EXP +from .numbers.constants import TransportTuning from .options import Options from .util import hostportjoin, hostportsplit, Sentinel, quote_nonascii from .util.uri import quote_factory, unreserved, sub_delims @@ -67,6 +67,14 @@ class Message(object): While a message has not been transmitted, the property is managed by the :class:`.Message` itself using the :meth:`.set_request_uri()` or the constructor `uri` argument. + * :attr:`transport_tuning`: Parameters used by one or more transports to + guide transmission. These are purely advisory hints; unknown properties + of that object are ignored, and transports consider them over built-in + constants on a best-effort basis. + + Note that many attributes are mandatory if this is not None; it is + recommended that any objects passed in here are based on the + :class:`aiocoap.numbers.constants.TransportTuning` class. * :attr:`request`: The request to which an incoming response message belongs; only available at the client. Managed by the @@ -129,7 +137,7 @@ class Message(object): * Some options or even the payload may differ if a proxy was involved. """ - def __init__(self, *, mtype=None, mid=None, code=None, payload=b'', token=b'', uri=None, **kwargs): + def __init__(self, *, mtype=None, mid=None, code=None, payload=b'', token=b'', uri=None, transport_tuning=None, **kwargs): self.version = 1 if mtype is None: # leave it unspecified for convenience, sending functions will know what to do @@ -148,6 +156,8 @@ def __init__(self, *, mtype=None, mid=None, code=None, payload=b'', token=b'', u self.remote = None + self.transport_tuning = transport_tuning or TransportTuning() + # deprecation error, should go away roughly after 0.2 release if self.payload is None: raise TypeError("Payload must not be None. Use empty string instead.") @@ -182,6 +192,9 @@ def copy(self, **kwargs): code=kwargs.pop('code', self.code), payload=kwargs.pop('payload', self.payload), token=kwargs.pop('token', self.token), + # Assuming these are not readily mutated, but rather passed + # around in a class-like fashion + transport_tuning=kwargs.pop('transport_tuning', self.transport_tuning), ) new.remote = kwargs.pop('remote', self.remote) new.opt = copy.deepcopy(self.opt) @@ -379,8 +392,8 @@ def _generate_next_block1_response(self): client with "more" flag set.""" response = Message(code=CHANGED, token=self.token) response.remote = self.remote - if self.opt.block1.block_number == 0 and self.opt.block1.size_exponent > DEFAULT_BLOCK_SIZE_EXP: - new_size_exponent = DEFAULT_BLOCK_SIZE_EXP + if self.opt.block1.block_number == 0 and self.opt.block1.size_exponent > self.transport_tuning.DEFAULT_BLOCK_SIZE_EXP: + new_size_exponent = self.transport_tuning.DEFAULT_BLOCK_SIZE_EXP response.opt.block1 = (0, True, new_size_exponent) else: response.opt.block1 = (self.opt.block1.block_number, True, self.opt.block1.size_exponent) diff --git a/aiocoap/messagemanager.py b/aiocoap/messagemanager.py index b30e2e7c..5f66d119 100644 --- a/aiocoap/messagemanager.py +++ b/aiocoap/messagemanager.py @@ -24,8 +24,6 @@ from .message import Message from .numbers.types import CON, ACK, RST, NON from .numbers.codes import EMPTY -from .numbers.constants import (EXCHANGE_LIFETIME, ACK_TIMEOUT, EMPTY_ACK_DELAY, - MAX_RETRANSMIT, ACK_RANDOM_FACTOR) class MessageManager(interfaces.TokenInterface, interfaces.MessageManager): @@ -179,7 +177,7 @@ def _deduplicate_message(self, message): return True else: self.log.debug('New unique message received') - self.loop.call_later(EXCHANGE_LIFETIME, functools.partial(self._recent_messages.pop, key)) + self.loop.call_later(message.transport_tuning.EXCHANGE_LIFETIME, functools.partial(self._recent_messages.pop, key)) self._recent_messages[key] = None return False @@ -207,7 +205,7 @@ def _add_exchange(self, message, messageerror_monitor): if message.remote not in self._backlogs: self._backlogs[message.remote] = [] - timeout = random.uniform(ACK_TIMEOUT, ACK_TIMEOUT * ACK_RANDOM_FACTOR) + timeout = random.uniform(message.transport_tuning.ACK_TIMEOUT, message.transport_tuning.ACK_TIMEOUT * message.transport_tuning.ACK_RANDOM_FACTOR) next_retransmission = self._schedule_retransmit(message, timeout, 0) self._active_exchanges[key] = (messageerror_monitor, next_retransmission) @@ -280,7 +278,7 @@ def _retransmit(self, message, timeout, retransmission_counter): # this should be a no-op, but let's be sure next_retransmission.cancel() - if retransmission_counter < MAX_RETRANSMIT: + if retransmission_counter < message.transport_tuning.MAX_RETRANSMIT: self.log.info("Retransmission, Message ID: %d.", message.mid) self._send_via_transport(message) retransmission_counter += 1 @@ -315,7 +313,7 @@ def on_timeout(self, remote, token): (remote, token)) self._send_empty_ack(request.remote, mid, "Response took too long to prepare") - handle = self.loop.call_later(EMPTY_ACK_DELAY, + handle = self.loop.call_later(request.transport_tuning.EMPTY_ACK_DELAY, on_timeout, self, request.remote, request.token) key = (request.remote, request.token) if key in self._piggyback_opportunities: diff --git a/aiocoap/numbers/__init__.py b/aiocoap/numbers/__init__.py index 8c10f402..575684fe 100644 --- a/aiocoap/numbers/__init__.py +++ b/aiocoap/numbers/__init__.py @@ -15,6 +15,9 @@ :class:`.optionnumbers.OptionNumber` classes are accessible in the same way. """ +import warnings +import string + from . import constants, types, codes # flake8 doesn't see through the global re-export from .constants import * # noqa: F401 F403 @@ -27,3 +30,9 @@ media_types = _MediaTypes() media_types_rev = _MediaTypesRev() + +def __getattr__(name): + if name[0] in string.ascii_uppercase and hasattr(constants._default_transport_tuning, name): + warnings.warn(f"{name} is deprecated, use through the message's transport_tuning instead", DeprecationWarning, stacklevel=2) + return getattr(constants._default_transport_tuning, name) + raise AttributeError(f"module {__name__} has no attribute {name}") diff --git a/aiocoap/numbers/constants.py b/aiocoap/numbers/constants.py index 93376823..f7dd4e8c 100644 --- a/aiocoap/numbers/constants.py +++ b/aiocoap/numbers/constants.py @@ -11,6 +11,9 @@ invented here for practical purposes of the implementation (eg. DEFAULT_BLOCK_SIZE_EXP, EMPTY_ACK_DELAY).""" +import warnings +import string + COAP_PORT = 5683 """The IANA-assigned standard port for COAP services.""" @@ -29,95 +32,126 @@ MCAST_IPV6_SITELOCAL_ALLCOAPNODES, ) -# +-------------------+---------------+ -# | name | default value | -# +-------------------+---------------+ -# | ACK_TIMEOUT | 2 seconds | -# | ACK_RANDOM_FACTOR | 1.5 | -# | MAX_RETRANSMIT | 4 | -# | NSTART | 1 | -# | DEFAULT_LEISURE | 5 seconds | -# | PROBING_RATE | 1 Byte/second | -# +-------------------+---------------+ - -ACK_TIMEOUT = 2.0 -"""The time, in seconds, to wait for an acknowledgement of a -confirmable message. The inter-transmission time doubles -for each retransmission.""" - -ACK_RANDOM_FACTOR = 1.5 -"""Timeout multiplier for anti-synchronization.""" - -MAX_RETRANSMIT = 4 -"""The number of retransmissions of confirmable messages to -non-multicast endpoints before the infrastructure assumes no -acknowledgement will be received.""" - -NSTART = 1 -"""Maximum number of simultaneous outstanding interactions - that endpoint maintains to a given server (including proxies)""" - -# +-------------------+---------------+ -# | name | default value | -# +-------------------+---------------+ -# | MAX_TRANSMIT_SPAN | 45 s | -# | MAX_TRANSMIT_WAIT | 93 s | -# | MAX_LATENCY | 100 s | -# | PROCESSING_DELAY | 2 s | -# | MAX_RTT | 202 s | -# | EXCHANGE_LIFETIME | 247 s | -# | NON_LIFETIME | 145 s | -# +-------------------+---------------+ - -MAX_TRANSMIT_SPAN = ACK_TIMEOUT * (2 ** MAX_RETRANSMIT - 1) * ACK_RANDOM_FACTOR -"""Maximum time from the first transmission -of a confirmable message to its last retransmission.""" - -MAX_TRANSMIT_WAIT = ACK_TIMEOUT * (2 ** (MAX_RETRANSMIT + 1) - 1) * ACK_RANDOM_FACTOR -"""Maximum time from the first transmission -of a confirmable message to the time when the sender gives up on -receiving an acknowledgement or reset.""" - -MAX_LATENCY = 100.0 -"""Maximum time a datagram is expected to take from the start -of its transmission to the completion of its reception.""" - -PROCESSING_DELAY = ACK_TIMEOUT -""""Time a node takes to turn around a -confirmable message into an acknowledgement.""" - -MAX_RTT = 2 * MAX_LATENCY + PROCESSING_DELAY -"""Maximum round-trip time.""" - -EXCHANGE_LIFETIME = MAX_TRANSMIT_SPAN + MAX_RTT -"""time from starting to send a confirmable message to the time when an -acknowledgement is no longer expected, i.e. message layer information about the -message exchange can be purged""" - -DEFAULT_BLOCK_SIZE_EXP = 6 # maximum block size 1024 -"""Default size exponent for blockwise transfers.""" - -EMPTY_ACK_DELAY = 0.1 -"""After this time protocol sends empty ACK, and separate response""" - -REQUEST_TIMEOUT = MAX_TRANSMIT_WAIT -"""Time after which server assumes it won't receive any answer. - It is not defined by IETF documents. - For human-operated devices it might be preferable to set some small value - (for example 10 seconds) - For M2M it's application dependent.""" - -DEFAULT_LEISURE = 5 - -MULTICAST_REQUEST_TIMEOUT = REQUEST_TIMEOUT + DEFAULT_LEISURE - -OBSERVATION_RESET_TIME = 128 -"""Time in seconds after which the value of the observe field are ignored. - -This number is not explicitly named in RFC7641. -""" +MAX_REGULAR_BLOCK_SIZE_EXP = 6 + +class TransportTuning: + """Base parameters that guide CoAP transport behaviors + + The values in here are recommended values, often defaults from RFCs. They + can be tuned in subclasses (and then passed into a message as + ``transport_tuning``), although users should be aware that alteing some of + these can cause the library to behave in ways violating the specification, + especially with respect to congestion control. + """ + + # +-------------------+---------------+ + # | name | default value | + # +-------------------+---------------+ + # | ACK_TIMEOUT | 2 seconds | + # | ACK_RANDOM_FACTOR | 1.5 | + # | MAX_RETRANSMIT | 4 | + # | NSTART | 1 | + # | DEFAULT_LEISURE | 5 seconds | + # | PROBING_RATE | 1 Byte/second | + # +-------------------+---------------+ + + ACK_TIMEOUT = 2.0 + """The time, in seconds, to wait for an acknowledgement of a + confirmable message. The inter-transmission time doubles + for each retransmission.""" + + ACK_RANDOM_FACTOR = 1.5 + """Timeout multiplier for anti-synchronization.""" + + MAX_RETRANSMIT = 4 + """The number of retransmissions of confirmable messages to + non-multicast endpoints before the infrastructure assumes no + acknowledgement will be received.""" + + NSTART = 1 + """Maximum number of simultaneous outstanding interactions + that endpoint maintains to a given server (including proxies)""" + + # +-------------------+---------------+ + # | name | default value | + # +-------------------+---------------+ + # | MAX_TRANSMIT_SPAN | 45 s | + # | MAX_TRANSMIT_WAIT | 93 s | + # | MAX_LATENCY | 100 s | + # | PROCESSING_DELAY | 2 s | + # | MAX_RTT | 202 s | + # | EXCHANGE_LIFETIME | 247 s | + # | NON_LIFETIME | 145 s | + # +-------------------+---------------+ + + @property + def MAX_TRANSMIT_SPAN(self): + """Maximum time from the first transmission + of a confirmable message to its last retransmission.""" + return self.ACK_TIMEOUT * (2 ** self.MAX_RETRANSMIT - 1) * self.ACK_RANDOM_FACTOR + + @property + def MAX_TRANSMIT_WAIT(self): + """Maximum time from the first transmission + of a confirmable message to the time when the sender gives up on + receiving an acknowledgement or reset.""" + return self.ACK_TIMEOUT * (2 ** (self.MAX_RETRANSMIT + 1) - 1) * self.ACK_RANDOM_FACTOR + + MAX_LATENCY = 100.0 + """Maximum time a datagram is expected to take from the start + of its transmission to the completion of its reception.""" + + @property + def PROCESSING_DELAY(self): + """"Time a node takes to turn around a + confirmable message into an acknowledgement.""" + return self.ACK_TIMEOUT + + @property + def MAX_RTT(self): + """Maximum round-trip time.""" + return 2 * self.MAX_LATENCY + self.PROCESSING_DELAY + + @property + def EXCHANGE_LIFETIME(self): + """time from starting to send a confirmable message to the time when an + acknowledgement is no longer expected, i.e. message layer information about the + message exchange can be purged""" + return self.MAX_TRANSMIT_SPAN + self.MAX_RTT + + DEFAULT_BLOCK_SIZE_EXP = MAX_REGULAR_BLOCK_SIZE_EXP + """Default size exponent for blockwise transfers.""" + + EMPTY_ACK_DELAY = 0.1 + """After this time protocol sends empty ACK, and separate response""" + + REQUEST_TIMEOUT = MAX_TRANSMIT_WAIT + """Time after which server assumes it won't receive any answer. + It is not defined by IETF documents. + For human-operated devices it might be preferable to set some small value + (for example 10 seconds) + For M2M it's application dependent.""" + + DEFAULT_LEISURE = 5 + + @property + def MULTICAST_REQUEST_TIMEOUT(self): + return self.REQUEST_TIMEOUT + self.DEFAULT_LEISURE + + OBSERVATION_RESET_TIME = 128 + """Time in seconds after which the value of the observe field are ignored. + + This number is not explicitly named in RFC7641. + """ + +_default_transport_tuning = TransportTuning() +def __getattr__(name): + if name[0] in string.ascii_uppercase and hasattr(_default_transport_tuning, name): + warnings.warn(f"{name} is deprecated, use through the message's transport_tuning instead", DeprecationWarning, stacklevel=2) + return getattr(_default_transport_tuning, name) + raise AttributeError(f"module {__name__} has no attribute {name}") SHUTDOWN_TIMEOUT = 3 """Maximum time, in seconds, for which the process is kept around during shutdown""" -__all__ = [k for k in dir() if not k.startswith('_')] +__all__ = [k for k in dir() if not k.startswith('_') and k not in ('warnings', 'strings')] diff --git a/aiocoap/protocol.py b/aiocoap/protocol.py index dcb30180..8e9c7ecf 100644 --- a/aiocoap/protocol.py +++ b/aiocoap/protocol.py @@ -57,7 +57,7 @@ from . import interfaces from . import error from .numbers import (INTERNAL_SERVER_ERROR, NOT_FOUND, - CONTINUE, OBSERVATION_RESET_TIME, SHUTDOWN_TIMEOUT) + CONTINUE, SHUTDOWN_TIMEOUT) from .util.asyncio import py38args import warnings @@ -555,7 +555,7 @@ def _run(self): is_recent = (v1 < v2 and v2 - v1 < 2**23) or \ (v1 > v2 and v1 - v2 > 2**23) or \ - (t2 > t1 + OBSERVATION_RESET_TIME) + (t2 > t1 + self._pipe.request.transport_tuning.OBSERVATION_RESET_TIME) if is_recent: t1 = t2 v1 = v2 diff --git a/aiocoap/transports/oscore.py b/aiocoap/transports/oscore.py index efe31220..b2df70f9 100644 --- a/aiocoap/transports/oscore.py +++ b/aiocoap/transports/oscore.py @@ -40,7 +40,7 @@ from functools import wraps from .. import interfaces, credentials, oscore -from ..numbers import UNAUTHORIZED +from ..numbers import UNAUTHORIZED, MAX_REGULAR_BLOCK_SIZE_EXP from ..util.asyncio import py38args class OSCOREAddress( @@ -92,7 +92,7 @@ def authenticated_claims(self): is_multicast = False maximum_payload_size = 1024 - maximum_block_size_exp = 6 + maximum_block_size_exp = MAX_REGULAR_BLOCK_SIZE_EXP @property def blockwise_key(self): diff --git a/contrib/oscore-plugtest/plugtest-server b/contrib/oscore-plugtest/plugtest-server index 4d5dceb0..c722414a 100755 --- a/contrib/oscore-plugtest/plugtest-server +++ b/contrib/oscore-plugtest/plugtest-server @@ -138,7 +138,7 @@ class InnerBlockMixin: # handler some day -- right now, i'm only doing the absolute minimum # necessary to satisfy the use case - inner_default_szx = aiocoap.DEFAULT_BLOCK_SIZE_EXP + inner_default_szx = aiocoap.MAX_REGULAR_BLOCK_SIZE_EXP async def render(self, request): response = await super().render(request)