Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Constant generalization #294

Merged
merged 6 commits into from
Mar 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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