The net_timeout structure is documented to exist because of behavior that is no longer true, i.e. that `k_delayed_work_submit()` supports only delays up to INT32_MAX milliseconds. Nonetheless, use of 32-bit timestamps within the work handlers mean the restriction is still present. This infrastructure is currently used for two timers with long durations: * address for IPv6 addresses * prefix for IPv6 prefixes The handling of rollover was subtly different between these: address wraps reset the start time while prefix wraps did not. The calculation of remaining time in ipv6_nbr was incorrect when the original requested time in seconds was a multiple of NET_TIMEOUT_MAX_VALUE: the remainder value would be zero while the wrap counter was positive, causing the calculation to indicate no time remained. The maximum value was set to allow a 100 ms latency between elapse of the deadline and assessment of a given timer, but detection of rollover assumed that the captured time in the work handler was precisely the expected deadline, which is unlikely to be true. Use of the shared system work queue also risks observed latency exceeding 100 ms. These calculations could produce delays to next event that exceeded the maximum delay, which introduced special cases. Refactor so all operations that use this structure are encapsulated into API that is documented and has a full-coverage unit test. Switch to the standard mechanism of detecting completed deadlines by calculating the signed difference between the deadline and the current time, which eliminates some special cases. Uniformly rely on the scanning the set of timers to determine the next deadline, rather than assuming that the most recent update is always next. Signed-off-by: Peter Bigot <peter.bigot@nordicsemi.no>
139 lines
3.7 KiB
C
139 lines
3.7 KiB
C
/*
|
|
* Copyright (c) 2020 Nordic Semiconductor ASA
|
|
*
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
#include <net/net_timeout.h>
|
|
#include <sys_clock.h>
|
|
|
|
void net_timeout_set(struct net_timeout *timeout,
|
|
uint32_t lifetime,
|
|
uint32_t now)
|
|
{
|
|
uint64_t expire_timeout;
|
|
|
|
timeout->timer_start = now;
|
|
|
|
/* Highly unlikely, but a zero timeout isn't correctly handled by the
|
|
* standard calculation.
|
|
*/
|
|
if (lifetime == 0U) {
|
|
timeout->wrap_counter = 0;
|
|
timeout->timer_timeout = 0;
|
|
return;
|
|
}
|
|
|
|
expire_timeout = (uint64_t)MSEC_PER_SEC * (uint64_t)lifetime;
|
|
timeout->wrap_counter = expire_timeout /
|
|
(uint64_t)NET_TIMEOUT_MAX_VALUE;
|
|
timeout->timer_timeout = expire_timeout -
|
|
(uint64_t)NET_TIMEOUT_MAX_VALUE *
|
|
(uint64_t)timeout->wrap_counter;
|
|
|
|
/* The implementation requires that the fractional timeout be zero
|
|
* only when the timeout has completed, so if the residual is zero
|
|
* copy over one timeout from the wrap.
|
|
*/
|
|
if (timeout->timer_timeout == 0U) {
|
|
timeout->timer_timeout = NET_TIMEOUT_MAX_VALUE;
|
|
timeout->wrap_counter -= 1;
|
|
}
|
|
}
|
|
|
|
int64_t net_timeout_deadline(const struct net_timeout *timeout,
|
|
int64_t now)
|
|
{
|
|
uint64_t start;
|
|
uint64_t deadline;
|
|
|
|
/* Reconstruct the full-precision start time assuming that the full
|
|
* precision start time is less than 2^32 ticks in the past.
|
|
*/
|
|
start = (uint64_t)now;
|
|
start -= (uint32_t)now - timeout->timer_start;
|
|
|
|
/* Offset the start time by the full precision remaining delay. */
|
|
deadline = start + timeout->timer_timeout;
|
|
deadline += (uint64_t)NET_TIMEOUT_MAX_VALUE
|
|
* (uint64_t)timeout->wrap_counter;
|
|
|
|
return (int64_t)deadline;
|
|
}
|
|
|
|
uint32_t net_timeout_remaining(const struct net_timeout *timeout,
|
|
uint32_t now)
|
|
{
|
|
int64_t ret = timeout->timer_timeout;
|
|
|
|
ret += timeout->wrap_counter * (uint64_t)NET_TIMEOUT_MAX_VALUE;
|
|
ret -= (int64_t)(int32_t)(now - timeout->timer_start);
|
|
if (ret <= 0) {
|
|
return 0;
|
|
}
|
|
|
|
return (uint32_t)((uint64_t)ret / MSEC_PER_SEC);
|
|
}
|
|
|
|
uint32_t net_timeout_evaluate(struct net_timeout *timeout,
|
|
uint32_t now)
|
|
{
|
|
uint32_t elapsed;
|
|
uint32_t last_delay;
|
|
int32_t remains;
|
|
bool wraps;
|
|
|
|
/* Time since last evaluation or set. */
|
|
elapsed = now - timeout->timer_start;
|
|
|
|
/* The delay used the last time this was evaluated. */
|
|
wraps = (timeout->wrap_counter > 0U);
|
|
last_delay = wraps
|
|
? NET_TIMEOUT_MAX_VALUE
|
|
: timeout->timer_timeout;
|
|
|
|
/* Time remaining until completion of the last delay. */
|
|
remains = (int32_t)(last_delay - elapsed);
|
|
|
|
/* If the deadline for the next event hasn't been reached yet just
|
|
* return the remaining time.
|
|
*/
|
|
if (remains > 0) {
|
|
return (uint32_t)remains;
|
|
}
|
|
|
|
/* Deadline has been reached. If we're not wrapping we've completed
|
|
* the last portion of the full timeout, so return zero to indicate
|
|
* the timeout has completed.
|
|
*/
|
|
if (!wraps) {
|
|
return 0U;
|
|
}
|
|
|
|
/* There's more to do. We need to update timer_start to correspond to
|
|
* now, then reduce the remaining time by the elapsed time. We know
|
|
* that's at least NET_TIMEOUT_MAX_VALUE, and can apply the
|
|
* reduction by decrementing the wrap count.
|
|
*/
|
|
timeout->timer_start = now;
|
|
elapsed -= NET_TIMEOUT_MAX_VALUE;
|
|
timeout->wrap_counter -= 1;
|
|
|
|
/* The residual elapsed must reduce timer_timeout, which is capped at
|
|
* NET_TIMEOUT_MAX_VALUE. But if subtracting would reduce the
|
|
* counter to zero or go negative we need to reduce the the wrap
|
|
* counter once more and add the residual to the counter, so the
|
|
* counter remains positive.
|
|
*/
|
|
if (timeout->timer_timeout > elapsed) {
|
|
timeout->timer_timeout -= elapsed;
|
|
} else {
|
|
timeout->timer_timeout += NET_TIMEOUT_MAX_VALUE - elapsed;
|
|
timeout->wrap_counter -= 1U;
|
|
}
|
|
|
|
return (timeout->wrap_counter == 0U)
|
|
? timeout->timer_timeout
|
|
: NET_TIMEOUT_MAX_VALUE;
|
|
}
|