chromium/chromeos/ash/services/device_sync/sync_scheduler_impl.cc

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

#include "chromeos/ash/services/device_sync/sync_scheduler_impl.h"

#include <algorithm>
#include <cmath>
#include <limits>
#include <memory>

#include "base/functional/bind.h"
#include "base/numerics/safe_conversions.h"
#include "base/rand_util.h"
#include "base/strings/stringprintf.h"
#include "chromeos/ash/components/multidevice/logging/logging.h"

namespace ash {

namespace device_sync {

namespace {

// Returns a human readable string given a |time_delta|.
std::string TimeDeltaToString(const base::TimeDelta& time_delta) {
  if (time_delta.InDays() > 0)
    return base::StringPrintf("%d days", time_delta.InDays());

  if (time_delta.InHours() > 0)
    return base::StringPrintf("%d hours", time_delta.InHours());

  if (time_delta.InMinutes() > 0)
    return base::StringPrintf("%d minutes", time_delta.InMinutes());

  return base::StringPrintf("%d seconds",
                            base::saturated_cast<int>(time_delta.InSeconds()));
}

}  // namespace

SyncSchedulerImpl::SyncSchedulerImpl(Delegate* delegate,
                                     base::TimeDelta refresh_period,
                                     base::TimeDelta base_recovery_period,
                                     double max_jitter_ratio,
                                     const std::string& scheduler_name)
    : delegate_(delegate),
      refresh_period_(refresh_period),
      base_recovery_period_(base_recovery_period),
      max_jitter_ratio_(max_jitter_ratio),
      scheduler_name_(scheduler_name),
      strategy_(Strategy::PERIODIC_REFRESH),
      sync_state_(SyncState::NOT_STARTED),
      failure_count_(0) {}

SyncSchedulerImpl::~SyncSchedulerImpl() {}

void SyncSchedulerImpl::Start(
    const base::TimeDelta& elapsed_time_since_last_sync,
    Strategy strategy) {
  strategy_ = strategy;
  sync_state_ = SyncState::WAITING_FOR_REFRESH;
  // We reset the failure backoff when the scheduler is started again, as the
  // configuration that caused the previous attempts to fail most likely won't
  // be present after a restart.
  if (strategy_ == Strategy::AGGRESSIVE_RECOVERY)
    failure_count_ = 1;

  // To take into account the time waited when the system is powered off, we
  // subtract the time elapsed with a normal sync period to the initial time
  // to wait.
  base::TimeDelta sync_delta =
      GetJitteredPeriod() - elapsed_time_since_last_sync;

  // The elapsed time may be negative if the system clock is changed. In this
  // case, we immediately schedule a sync.
  base::TimeDelta zero_delta = base::Seconds(0);
  if (elapsed_time_since_last_sync < zero_delta || sync_delta < zero_delta)
    sync_delta = zero_delta;

  ScheduleNextSync(sync_delta);
}

void SyncSchedulerImpl::ForceSync() {
  OnTimerFired();
}

base::TimeDelta SyncSchedulerImpl::GetTimeToNextSync() const {
  if (!timer_)
    return base::Seconds(0);
  return timer_->GetCurrentDelay();
}

SyncScheduler::Strategy SyncSchedulerImpl::GetStrategy() const {
  return strategy_;
}

SyncScheduler::SyncState SyncSchedulerImpl::GetSyncState() const {
  return sync_state_;
}

void SyncSchedulerImpl::OnTimerFired() {
  timer_.reset();
  if (strategy_ == Strategy::PERIODIC_REFRESH) {
    PA_LOG(VERBOSE) << "Timer fired for periodic refresh, making request...";
    sync_state_ = SyncState::SYNC_IN_PROGRESS;
  } else if (strategy_ == Strategy::AGGRESSIVE_RECOVERY) {
    PA_LOG(VERBOSE) << "Timer fired for aggressive recovery, making request...";
    sync_state_ = SyncState::SYNC_IN_PROGRESS;
  } else {
    NOTREACHED_IN_MIGRATION();
    return;
  }

  delegate_->OnSyncRequested(
      std::make_unique<SyncRequest>(weak_ptr_factory_.GetWeakPtr()));
}

std::unique_ptr<base::OneShotTimer> SyncSchedulerImpl::CreateTimer() {
  return std::make_unique<base::OneShotTimer>();
}

void SyncSchedulerImpl::ScheduleNextSync(const base::TimeDelta& sync_delta) {
  if (sync_state_ != SyncState::WAITING_FOR_REFRESH) {
    PA_LOG(ERROR) << "Unexpected state when scheduling next sync: sync_state="
                  << static_cast<int>(sync_state_);
    return;
  }

  bool is_aggressive_recovery = (strategy_ == Strategy::AGGRESSIVE_RECOVERY);
  PA_LOG(VERBOSE) << "Scheduling next sync for " << scheduler_name_ << ":\n"
                  << "    Strategy: "
                  << (is_aggressive_recovery ? "Aggressive Recovery"
                                             : "Periodic Refresh")
                  << "\n"
                  << "    Time Delta: " << TimeDeltaToString(sync_delta)
                  << (is_aggressive_recovery
                          ? base::StringPrintf(
                                "\n    Previous Failures: %d",
                                base::saturated_cast<int>(failure_count_))
                          : "");

  timer_ = CreateTimer();
  timer_->Start(FROM_HERE, sync_delta,
                base::BindOnce(&SyncSchedulerImpl::OnTimerFired,
                               weak_ptr_factory_.GetWeakPtr()));
}

void SyncSchedulerImpl::OnSyncCompleted(bool success) {
  if (sync_state_ != SyncState::SYNC_IN_PROGRESS) {
    PA_LOG(ERROR) << "Unexpected state when sync completed: sync_state="
                  << static_cast<int>(sync_state_)
                  << ", strategy_=" << static_cast<int>(strategy_);
    return;
  }
  sync_state_ = SyncState::WAITING_FOR_REFRESH;

  if (success) {
    strategy_ = Strategy::PERIODIC_REFRESH;
    failure_count_ = 0;
  } else {
    strategy_ = Strategy::AGGRESSIVE_RECOVERY;
    ++failure_count_;
  }

  ScheduleNextSync(GetJitteredPeriod());
}

base::TimeDelta SyncSchedulerImpl::GetJitteredPeriod() {
  double jitter = 2 * max_jitter_ratio_ * (base::RandDouble() - 0.5);
  base::TimeDelta period = GetPeriod();
  base::TimeDelta jittered_time_delta = period + (period * jitter);
  if (jittered_time_delta.InMilliseconds() < 0)
    jittered_time_delta = base::Milliseconds(0);
  return jittered_time_delta;
}

base::TimeDelta SyncSchedulerImpl::GetPeriod() {
  if (strategy_ == Strategy::PERIODIC_REFRESH)
    return refresh_period_;
  if (strategy_ == Strategy::AGGRESSIVE_RECOVERY && failure_count_ > 0) {
    // The backoff for each consecutive failure is exponentially doubled until
    // it is equal to the normal refresh period.
    // Note: |backoff_factor| may evaulate to INF if |failure_count_| is large,
    // but multiplication operations for TimeDelta objects are saturated.
    double backoff_factor = pow(2, failure_count_ - 1);
    base::TimeDelta backoff_period = base_recovery_period_ * backoff_factor;
    return backoff_period < refresh_period_ ? backoff_period : refresh_period_;
  }
  PA_LOG(ERROR) << "Error getting period for strategy: "
                << static_cast<int>(strategy_);
  return base::TimeDelta();
}

}  // namespace device_sync

}  // namespace ash