Skip to content

Commit

Permalink
numbers.constants: Deprecate global transport related parameters in f…
Browse files Browse the repository at this point in the history
…avor of per-message instance

This allows setting properties like the ACK_TIMEOUT or the number of
retransmissions on a per-message basis. This allows better
customization, especially for clients.

Their old global names are deprecated.

There is a subtle breaking change in this: The now deprecated globals
are not available through wildcard imports any more -- if they were, no
deprecation warnings could be raised for them.

Closes: #175
Merges: #294
  • Loading branch information
chrysn committed Mar 28, 2023
2 parents ac19e30 + 314267e commit 61a73ff
Show file tree
Hide file tree
Showing 9 changed files with 161 additions and 107 deletions.
4 changes: 2 additions & 2 deletions aiocoap/blockwise.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions aiocoap/interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
21 changes: 17 additions & 4 deletions aiocoap/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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.")
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
10 changes: 4 additions & 6 deletions aiocoap/messagemanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
9 changes: 9 additions & 0 deletions aiocoap/numbers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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}")
210 changes: 122 additions & 88 deletions aiocoap/numbers/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""

Expand All @@ -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')]
Loading

0 comments on commit 61a73ff

Please sign in to comment.