// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "components/sync/engine/backoff_delay_provider.h"

#include <algorithm>

#include "base/feature_list.h"
#include "base/memory/ptr_util.h"
#include "base/rand_util.h"
#include "components/sync/engine/cycle/model_neutral_state.h"
#include "components/sync/engine/polling_constants.h"
#include "components/sync/engine/sync_protocol_error.h"
#include "components/sync/engine/syncer_error.h"

namespace syncer {

namespace {

// If enabled, the initial delay on network error will be 1 second instead of
// 30 seconds.
BASE_FEATURE(kSyncShortInitialDelayOnNetworkError,
             base::FEATURE_ENABLED_BY_DEFAULT);

// This calculates approx. last_delay * kBackoffMultiplyFactor +/- last_delay
// * kBackoffJitterFactor. `jitter_sign` must be -1 or 1 and determines whether
// the jitter in the delay will be positive or negative.
base::TimeDelta GetDelayImpl(base::TimeDelta last_delay, int jitter_sign) {
  DCHECK(jitter_sign == -1 || jitter_sign == 1);

  if (last_delay >= kMaxBackoffTime) {
    return kMaxBackoffTime;
  }

  const base::TimeDelta backoff =
      std::max(kMinBackoffTime, last_delay * kBackoffMultiplyFactor) +
      jitter_sign * kBackoffJitterFactor * last_delay;

  // Clamp backoff between 1 second and `kMaxBackoffTime`.
  return std::max(kMinBackoffTime, std::min(backoff, kMaxBackoffTime));
}

}  // namespace

// static
std::unique_ptr<BackoffDelayProvider> BackoffDelayProvider::FromDefaults() {
  // base::WrapUnique() used because the constructor is private.
  if (base::FeatureList::IsEnabled(kSyncShortInitialDelayOnNetworkError)) {
    return base::WrapUnique(new BackoffDelayProvider(
        kInitialBackoffRetryTime, kInitialBackoffShortRetryTime));
  }
  return base::WrapUnique(new BackoffDelayProvider(
      kInitialBackoffRetryTime, kInitialBackoffImmediateRetryTime));
}

// static
std::unique_ptr<BackoffDelayProvider>
BackoffDelayProvider::WithShortInitialRetryOverride() {
  // base::WrapUnique() used because the constructor is private.
  return base::WrapUnique(new BackoffDelayProvider(
      kInitialBackoffShortRetryTime, kInitialBackoffImmediateRetryTime));
}

BackoffDelayProvider::BackoffDelayProvider(
    const base::TimeDelta& default_initial_backoff,
    const base::TimeDelta& short_initial_backoff)
    : default_initial_backoff_(default_initial_backoff),
      short_initial_backoff_(short_initial_backoff) {}

BackoffDelayProvider::~BackoffDelayProvider() = default;

base::TimeDelta BackoffDelayProvider::GetDelay(
    const base::TimeDelta& last_delay) {
  // Flip a coin to randomize backoff interval by +/- kBackoffJitterFactor.
  const int jitter_sign = base::RandIntInclusive(0, 1) * 2 - 1;
  return GetDelayImpl(last_delay, jitter_sign);
}

base::TimeDelta BackoffDelayProvider::GetInitialDelay(
    const ModelNeutralState& state) const {
  // kNetworkError implies we did not receive HTTP response from server because
  // of some network error. If network is unavailable then on next connection
  // type or address change scheduler will run canary job. Otherwise we'll retry
  // in 30 seconds.
  if (state.commit_result.type() == SyncerError::Type::kNetworkError ||
      state.last_download_updates_result.type() ==
          SyncerError::Type::kNetworkError) {
    if (base::FeatureList::IsEnabled(kSyncShortInitialDelayOnNetworkError)) {
      // Retry with a short delay on network error. This should be safe because
      // it shouldn't increase traffic to the server but should help to recover
      // from the network errors faster.
      return short_initial_backoff_;
    }
    return default_initial_backoff_;
  }

  if (state.last_get_key_failed) {
    return default_initial_backoff_;
  }

  // Note: If we received a MIGRATION_DONE on download updates, then commit
  // should not have taken place.  Moreover, if we receive a MIGRATION_DONE
  // on commit, it means that download updates succeeded.  Therefore, we only
  // need to check if either code is equal to SERVER_RETURN_MIGRATION_DONE,
  // and not if there were any more serious errors requiring the long retry.
  if (state.last_download_updates_result.type() ==
          SyncerError::Type::kProtocolError &&
      state.last_download_updates_result.GetProtocolErrorOrDie() ==
          SyncProtocolErrorType::MIGRATION_DONE) {
    return kInitialBackoffImmediateRetryTime;
  }
  if (state.commit_result.type() == SyncerError::Type::kProtocolError &&
      state.commit_result.GetProtocolErrorOrDie() ==
          SyncProtocolErrorType::MIGRATION_DONE) {
    return kInitialBackoffImmediateRetryTime;
  }

  // When the server tells us we have a conflict, then we should download the
  // latest updates so we can see the conflict ourselves, resolve it locally,
  // then try again to commit.  Running another sync cycle will do all these
  // things.  There's no need to back off, we can do this immediately.
  //
  // TODO(sync): We shouldn't need to handle this in BackoffDelayProvider.
  // There should be a way to deal with protocol errors before we get to this
  // point.
  if (state.commit_result.type() == SyncerError::Type::kProtocolError &&
      state.commit_result.GetProtocolErrorOrDie() ==
          SyncProtocolErrorType::CONFLICT) {
    return kInitialBackoffImmediateRetryTime;
  }

  return default_initial_backoff_;
}

base::TimeDelta BackoffDelayProvider::GetDelayForTesting(
    base::TimeDelta last_delay,
    int jitter_sign) {
  return GetDelayImpl(last_delay, jitter_sign);
}

}  // namespace syncer
