// Copyright 2022 gRPC authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include "src/core/lib/resource_quota/periodic_update.h"

#include <grpc/support/port_platform.h>

#include <atomic>

#include "src/core/util/useful.h"

namespace grpc_core {

bool PeriodicUpdate::Interrupt(absl::FunctionRef<void(Duration)> f) {
  int64_t old_value = updates_remaining_.load(std::memory_order_acquire);
  while (true) {
    if (old_value <= 1) return false;
    if (!updates_remaining_.compare_exchange_weak(old_value, 0,
                                                  std::memory_order_acquire)) {
      continue;
    }
    f(Timestamp::Now() - period_start_);
    updates_remaining_.store(old_value, std::memory_order_release);
    return true;
  }
}

bool PeriodicUpdate::MaybeEndPeriod(absl::FunctionRef<void(Duration)> f) {
  if (period_start_ == Timestamp::ProcessEpoch()) {
    period_start_ = Timestamp::Now();
    updates_remaining_.store(1, std::memory_order_release);
    return false;
  }
  // updates_remaining_ just reached 0 and the thread calling this function was
  // the decrementer that got us there.
  // We can now safely mutate any non-atomic mutable variables (we've got a
  // guarantee that no other thread will), and by the time this function returns
  // we must store a positive number into updates_remaining_.
  auto now = Timestamp::Now();
  Duration time_so_far = now - period_start_;
  if (time_so_far < period_) {
    // At most double the number of updates remaining until the next period.
    // At least try to estimate when we'll reach it.
    int64_t better_guess;
    if (time_so_far.millis() == 0) {
      better_guess = expected_updates_per_period_ * 2;
    } else {
      // Determine a scaling factor that would have gotten us to the next
      // period, but clamp between 1.01 (at least 1% increase in guesses)
      // and 2.0 (at most doubling) - to avoid running completely out of
      // control.
      const double scale =
          Clamp(period_.seconds() / time_so_far.seconds(), 1.01, 2.0);
      better_guess = expected_updates_per_period_ * scale;
      if (better_guess <= expected_updates_per_period_) {
        better_guess = expected_updates_per_period_ + 1;
      }
    }
    // Store the remainder left. Note that updates_remaining_ may have been
    // decremented by another thread whilst we performed the above calculations:
    // we simply discard those decrements.
    updates_remaining_.store(better_guess - expected_updates_per_period_,
                             std::memory_order_release);
    // Not quite done, return, try for longer.
    return false;
  }
  // Finished period, start a new one and return true.
  // We try to predict how many update periods we'd need to cover the full time
  // span, and we increase that by 1% to attempt to tend to not enter the above
  // stanza.
  expected_updates_per_period_ =
      period_.seconds() * expected_updates_per_period_ / time_so_far.seconds();
  if (expected_updates_per_period_ < 1) expected_updates_per_period_ = 1;
  f(time_so_far);
  period_start_ = Timestamp::Now();
  updates_remaining_.store(expected_updates_per_period_,
                           std::memory_order_release);
  return true;
}

}  // namespace grpc_core
