/* Copyright (c) 2001-2004, Roger Dingledine.
* Copyright (c) 2004-2006, Roger Dingledine, Nick Mathewson.
* Copyright (c) 2007-2020, The Tor Project, Inc. */
/* See LICENSE for licensing information */
/**
* @file dlstatus.c
* @brief Track status and retry schedule of a downloadable object.
**/
#define DLSTATUS_PRIVATE
#include "core/or/or.h"
#include "app/config/config.h"
#include "feature/client/entrynodes.h"
#include "feature/dirclient/dlstatus.h"
#include "feature/nodelist/networkstatus.h"
#include "feature/relay/routermode.h"
#include "lib/crypt_ops/crypto_rand.h"
#include "feature/dirclient/download_status_st.h"
/** Decide which download schedule we want to use based on descriptor type
* in dls and options.
*
* Then, return the initial delay for that download schedule, in seconds.
*
* Helper function for download_status_increment_failure(),
* download_status_reset(), and download_status_increment_attempt(). */
STATIC int
find_dl_min_delay(const download_status_t *dls, const or_options_t *options)
{
tor_assert(dls);
tor_assert(options);
switch (dls->schedule) {
case DL_SCHED_GENERIC:
/* Any other directory document */
if (dir_server_mode(options)) {
/* A directory authority or directory mirror */
return options->TestingServerDownloadInitialDelay;
} else {
return options->TestingClientDownloadInitialDelay;
}
case DL_SCHED_CONSENSUS:
if (!networkstatus_consensus_can_use_multiple_directories(options)) {
/* A public relay */
return options->TestingServerConsensusDownloadInitialDelay;
} else {
/* A client or bridge */
if (networkstatus_consensus_is_bootstrapping(time(NULL))) {
/* During bootstrapping */
if (!networkstatus_consensus_can_use_extra_fallbacks(options)) {
/* A bootstrapping client without extra fallback directories */
return options->
ClientBootstrapConsensusAuthorityOnlyDownloadInitialDelay;
} else if (dls->want_authority) {
/* A bootstrapping client with extra fallback directories, but
* connecting to an authority */
return
options->ClientBootstrapConsensusAuthorityDownloadInitialDelay;
} else {
/* A bootstrapping client connecting to extra fallback directories
*/
return
options->ClientBootstrapConsensusFallbackDownloadInitialDelay;
}
} else {
/* A client with a reasonably live consensus, with or without
* certificates */
return options->TestingClientConsensusDownloadInitialDelay;
}
}
case DL_SCHED_BRIDGE:
if (options->UseBridges && num_bridges_usable(0) > 0) {
/* A bridge client that is sure that one or more of its bridges are
* running can afford to wait longer to update bridge descriptors. */
return options->TestingBridgeDownloadInitialDelay;
} else {
/* A bridge client which might have no running bridges, must try to
* get bridge descriptors straight away. */
return options->TestingBridgeBootstrapDownloadInitialDelay;
}
default:
tor_assert(0);
}
/* Impossible, but gcc will fail with -Werror without a `return`. */
return 0;
}
/** As next_random_exponential_delay() below, but does not compute a random
* value. Instead, compute the range of values that
* next_random_exponential_delay() should use when computing its random value.
* Store the low bound into *low_bound_out, and the high bound into
* *high_bound_out. Guarantees that the low bound is strictly less
* than the high bound. */
STATIC void
next_random_exponential_delay_range(int *low_bound_out,
int *high_bound_out,
int delay,
int base_delay)
{
// This is the "decorrelated jitter" approach, from
// https://www.awsarchitectureblog.com/2015/03/backoff.html
// The formula is
// sleep = min(cap, random_between(base, sleep * 3))
const int delay_times_3 = delay < INT_MAX/3 ? delay * 3 : INT_MAX;
*low_bound_out = base_delay;
if (delay_times_3 > base_delay) {
*high_bound_out = delay_times_3;
} else {
*high_bound_out = base_delay+1;
}
}
/** Advance one delay step. The algorithm will generate a random delay,
* such that each failure is possibly (random) longer than the ones before.
*
* We then clamp that value to be no larger than max_delay, and return it.
*
* The base_delay parameter is lowest possible delay time (can't be
* zero); the backoff_position parameter is the number of times we've
* generated a delay; and the delay argument is the most recently used
* delay.
*/
STATIC int
next_random_exponential_delay(int delay,
int base_delay)
{
/* Check preconditions */
if (BUG(delay < 0))
delay = 0;
if (base_delay < 1)
base_delay = 1;
int low_bound=0, high_bound=INT_MAX;
next_random_exponential_delay_range(&low_bound, &high_bound,
delay, base_delay);
return crypto_rand_int_range(low_bound, high_bound);
}
/** Find the current delay for dls based on min_delay.
*
* This function sets dls->next_attempt_at based on now, and returns the delay.
* Helper for download_status_increment_failure and
* download_status_increment_attempt. */
STATIC int
download_status_schedule_get_delay(download_status_t *dls,
int min_delay,
time_t now)
{
tor_assert(dls);
/* If we're using random exponential backoff, we do need min/max delay */
tor_assert(min_delay >= 0);
int delay = INT_MAX;
uint8_t dls_schedule_position = (dls->increment_on
== DL_SCHED_INCREMENT_ATTEMPT
? dls->n_download_attempts
: dls->n_download_failures);
/* Check if we missed a reset somehow */
IF_BUG_ONCE(dls->last_backoff_position > dls_schedule_position) {
dls->last_backoff_position = 0;
dls->last_delay_used = 0;
}
if (dls_schedule_position > 0) {
delay = dls->last_delay_used;
while (dls->last_backoff_position < dls_schedule_position) {
/* Do one increment step */
delay = next_random_exponential_delay(delay, min_delay);
/* Update our position */
++(dls->last_backoff_position);
}
} else {
/* If we're just starting out, use the minimum delay */
delay = min_delay;
}
/* Clamp it within min/max if we have them */
if (min_delay >= 0 && delay < min_delay) delay = min_delay;
/* Store it for next time */
dls->last_backoff_position = dls_schedule_position;
dls->last_delay_used = delay;
/* A negative delay makes no sense. Knowing that delay is
* non-negative allows us to safely do the wrapping check below. */
tor_assert(delay >= 0);
/* Avoid now+delay overflowing TIME_MAX, by comparing with a subtraction
* that won't overflow (since delay is non-negative). */
if (delay < INT_MAX && now <= TIME_MAX - delay) {
dls->next_attempt_at = now+delay;
} else {
dls->next_attempt_at = TIME_MAX;
}
return delay;
}
/* Log a debug message about item, which increments on increment_action, has
* incremented dls_n_download_increments times. The message varies based on
* was_schedule_incremented (if not, not_incremented_response is logged), and
* the values of increment, dls_next_attempt_at, and now.
* Helper for download_status_increment_failure and
* download_status_increment_attempt. */
static void
download_status_log_helper(const char *item, int was_schedule_incremented,
const char *increment_action,
const char *not_incremented_response,
uint8_t dls_n_download_increments, int increment,
time_t dls_next_attempt_at, time_t now)
{
if (item) {
if (!was_schedule_incremented)
log_debug(LD_DIR, "%s %s %d time(s); I'll try again %s.",
item, increment_action, (int)dls_n_download_increments,
not_incremented_response);
else if (increment == 0)
log_debug(LD_DIR, "%s %s %d time(s); I'll try again immediately.",
item, increment_action, (int)dls_n_download_increments);
else if (dls_next_attempt_at < TIME_MAX)
log_debug(LD_DIR, "%s %s %d time(s); I'll try again in %d seconds.",
item, increment_action, (int)dls_n_download_increments,
(int)(dls_next_attempt_at-now));
else
log_debug(LD_DIR, "%s %s %d time(s); Giving up for a while.",
item, increment_action, (int)dls_n_download_increments);
}
}
/** Determine when a failed download attempt should be retried.
* Called when an attempt to download dls has failed with HTTP status
* status_code. Increment the failure count (if the code indicates a
* real failure, or if we're a server) and set dls-\>next_attempt_at to
* an appropriate time in the future and return it.
* If dls->increment_on is DL_SCHED_INCREMENT_ATTEMPT, increment the
* failure count, and return a time in the far future for the next attempt (to
* avoid an immediate retry). */
time_t
download_status_increment_failure(download_status_t *dls, int status_code,
const char *item, int server, time_t now)
{
(void) status_code; // XXXX no longer used.
(void) server; // XXXX no longer used.
int increment = -1;
int min_delay = 0;
tor_assert(dls);
/* dls wasn't reset before it was used */
if (dls->next_attempt_at == 0) {
download_status_reset(dls);
}
/* count the failure */
if (dls->n_download_failures < IMPOSSIBLE_TO_DOWNLOAD-1) {
++dls->n_download_failures;
}
if (dls->increment_on == DL_SCHED_INCREMENT_FAILURE) {
/* We don't find out that a failure-based schedule has attempted a
* connection until that connection fails.
* We'll never find out about successful connections, but this doesn't
* matter, because schedules are reset after a successful download.
*/
if (dls->n_download_attempts < IMPOSSIBLE_TO_DOWNLOAD-1)
++dls->n_download_attempts;
/* only return a failure retry time if this schedule increments on failures
*/
min_delay = find_dl_min_delay(dls, get_options());
increment = download_status_schedule_get_delay(dls, min_delay, now);
}
download_status_log_helper(item, !dls->increment_on, "failed",
"concurrently", dls->n_download_failures,
increment,
download_status_get_next_attempt_at(dls),
now);
if (dls->increment_on == DL_SCHED_INCREMENT_ATTEMPT) {
/* stop this schedule retrying on failure, it will launch concurrent
* connections instead */
return TIME_MAX;
} else {
return download_status_get_next_attempt_at(dls);
}
}
/** Determine when the next download attempt should be made when using an
* attempt-based (potentially concurrent) download schedule.
* Called when an attempt to download dls is being initiated.
* Increment the attempt count and set dls-\>next_attempt_at to an
* appropriate time in the future and return it.
* If dls->increment_on is DL_SCHED_INCREMENT_FAILURE, don't increment
* the attempts, and return a time in the far future (to avoid launching a
* concurrent attempt). */
time_t
download_status_increment_attempt(download_status_t *dls, const char *item,
time_t now)
{
int delay = -1;
int min_delay = 0;
tor_assert(dls);
/* dls wasn't reset before it was used */
if (dls->next_attempt_at == 0) {
download_status_reset(dls);
}
if (dls->increment_on == DL_SCHED_INCREMENT_FAILURE) {
/* this schedule should retry on failure, and not launch any concurrent
attempts */
log_warn(LD_BUG, "Tried to launch an attempt-based connection on a "
"failure-based schedule.");
return TIME_MAX;
}
if (dls->n_download_attempts < IMPOSSIBLE_TO_DOWNLOAD-1)
++dls->n_download_attempts;
min_delay = find_dl_min_delay(dls, get_options());
delay = download_status_schedule_get_delay(dls, min_delay, now);
download_status_log_helper(item, dls->increment_on, "attempted",
"on failure", dls->n_download_attempts,
delay, download_status_get_next_attempt_at(dls),
now);
return download_status_get_next_attempt_at(dls);
}
static time_t
download_status_get_initial_delay_from_now(const download_status_t *dls)
{
/* We use constant initial delays, even in exponential backoff
* schedules. */
return time(NULL) + find_dl_min_delay(dls, get_options());
}
/** Reset dls so that it will be considered downloadable
* immediately, and/or to show that we don't need it anymore.
*
* Must be called to initialise a download schedule, otherwise the zeroth item
* in the schedule will never be used.
*
* (We find the zeroth element of the download schedule, and set
* next_attempt_at to be the appropriate offset from 'now'. In most
* cases this means setting it to 'now', so the item will be immediately
* downloadable; when using authorities with fallbacks, there is a few seconds'
* delay.) */
void
download_status_reset(download_status_t *dls)
{
if (dls->n_download_failures == IMPOSSIBLE_TO_DOWNLOAD
|| dls->n_download_attempts == IMPOSSIBLE_TO_DOWNLOAD)
return; /* Don't reset this. */
dls->n_download_failures = 0;
dls->n_download_attempts = 0;
dls->next_attempt_at = download_status_get_initial_delay_from_now(dls);
dls->last_backoff_position = 0;
dls->last_delay_used = 0;
/* Don't reset dls->want_authority or dls->increment_on */
}
/** Return true iff, as of now, the resource tracked by dls is
* ready to get its download reattempted. */
int
download_status_is_ready(download_status_t *dls, time_t now)
{
/* dls wasn't reset before it was used */
if (dls->next_attempt_at == 0) {
download_status_reset(dls);
}
return download_status_get_next_attempt_at(dls) <= now;
}
/** Mark dl as never downloadable. */
void
download_status_mark_impossible(download_status_t *dl)
{
dl->n_download_failures = IMPOSSIBLE_TO_DOWNLOAD;
dl->n_download_attempts = IMPOSSIBLE_TO_DOWNLOAD;
}
/** Return the number of failures on dls since the last success (if
* any). */
int
download_status_get_n_failures(const download_status_t *dls)
{
return dls->n_download_failures;
}
/** Return the number of attempts to download dls since the last success
* (if any). This can differ from download_status_get_n_failures() due to
* outstanding concurrent attempts. */
int
download_status_get_n_attempts(const download_status_t *dls)
{
return dls->n_download_attempts;
}
/** Return the next time to attempt to download dls. */
time_t
download_status_get_next_attempt_at(const download_status_t *dls)
{
/* dls wasn't reset before it was used */
if (dls->next_attempt_at == 0) {
/* so give the answer we would have given if it had been */
return download_status_get_initial_delay_from_now(dls);
}
return dls->next_attempt_at;
}