/*
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * 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.
 */

#pragma once

#include <folly/concurrency/CacheLocality.h>
#include <folly/concurrency/UnboundedQueue.h>

#include <glog/logging.h>

#include <atomic>
#include <chrono>

namespace folly {

/// DynamicBoundedQueue supports:
/// - Dynamic memory usage that grows and shrink in proportion to the
///   number of elements in the queue.
/// - Adjustable capacity that helps throttle pathological cases of
///   producer-consumer imbalance that may lead to excessive memory
///   usage.
/// - The adjustable capacity can also help prevent deadlock by
///   allowing users to temporarily increase capacity substantially to
///   guarantee accommodating producer requests that cannot wait.
/// - SPSC, SPMC, MPSC, MPMC variants.
/// - Blocking and spinning-only variants.
/// - Inter-operable non-waiting, timed until, timed for, and waiting
///   variants of producer and consumer operations.
/// - Optional variable element weights.
///
/// Element Weights
/// - Queue elements may have variable weights (calculated using a
///   template parameter) that are by default 1.
/// - Element weights count towards the queue's capacity.
/// - Elements weights are not priorities and do not affect element
///   order. Queues with variable element weights follow FIFO order,
///   the same as default queues.
///
/// When to use DynamicBoundedQueue:
/// - If a small maximum capacity may lead to deadlock or performance
///   degradation under bursty patterns and a larger capacity is
///   sufficient.
/// - If the typical queue size is expected to be much lower than the
///   maximum capacity
/// - If an unbounded queue is susceptible to growing too much.
/// - If support for variable element weights is needed.
///
/// When not to use DynamicBoundedQueue?
/// - If dynamic memory allocation is unacceptable or if the maximum
///   capacity needs to be small, then use fixed-size MPMCQueue or (if
///   non-blocking SPSC) ProducerConsumerQueue.
/// - If there is no risk of the queue growing too much, then use
///   UnboundedQueue.
///
/// Setting capacity
/// - The general rule is to set the capacity as high as acceptable.
///   The queue performs best when it is not near full capacity.
/// - The implementation may allow extra slack in capacity (~10%) for
///   amortizing some costly steps. Therefore, precise capacity is not
///   guaranteed and cannot be relied on for synchronization; i.e.,
///   this queue cannot be used as a semaphore.
///
/// Performance expectations:
/// - As long as the queue size is below capacity in the common case,
///   performance is comparable to MPMCQueue and better in cases of
///   higher producer demand.
/// - Performance degrades gracefully at full capacity.
/// - It is recommended to measure performance with different variants
///   when applicable, e.g., DMPMC vs DMPSC. Depending on the use
///   case, sometimes the variant with the higher sequential overhead
///   may yield better results due to, for example, more favorable
///   producer-consumer balance or favorable timing for avoiding
///   costly blocking.
/// - See DynamicBoundedQueueTest.cpp for some benchmark results.
///
/// Template parameters:
/// - T: element type
/// - SingleProducer: true if there can be only one producer at a
///   time.
/// - SingleConsumer: true if there can be only one consumer at a
///   time.
/// - MayBlock: true if producers or consumers may block.
/// - LgSegmentSize (default 8): Log base 2 of number of elements per
///   UnboundedQueue segment.
/// - LgAlign (default 7): Log base 2 of alignment directive; can be
///   used to balance scalability (avoidance of false sharing) with
///   memory efficiency.
/// - WeightFn (DefaultWeightFn<T>): A customizable weight computing type
///   for computing the weights of elements. The default weight is 1.
///
/// Template Aliases:
///   DSPSCQueue<T, MayBlock, LgSegmentSize, LgAlign>
///   DMPSCQueue<T, MayBlock, LgSegmentSize, LgAlign>
///   DSPMCQueue<T, MayBlock, LgSegmentSize, LgAlign>
///   DMPMCQueue<T, MayBlock, LgSegmentSize, LgAlign>
///
/// Functions:
///   Constructor
///     Takes a capacity value as an argument.
///
///   Producer functions:
///     void enqueue(const T&);
///     void enqueue(T&&);
///         Adds an element to the end of the queue. Waits until
///         capacity is available if necessary.
///     bool try_enqueue(const T&);
///     bool try_enqueue(T&&);
///         Tries to add an element to the end of the queue if
///         capacity allows it. Returns true if successful. Otherwise
///         Returns false.
///     bool try_enqueue_until(const T&, time_point& deadline);
///     bool try_enqueue_until(T&&, time_point& deadline);
///         Tries to add an element to the end of the queue if
///         capacity allows it until the specified deadline. Returns
///         true if successful, otherwise false.
///     bool try_enqueue_for(const T&, duration&);
///     bool try_enqueue_for(T&&, duration&);
///         Tries to add an element to the end of the queue if
///         capacity allows until the expiration of the specified
///         duration. Returns true if successful, otherwise false.
///
///   Consumer functions:
///     void dequeue(T&);
///         Extracts an element from the front of the queue. Waits
///         until an element is available if necessary.
///     bool try_dequeue(T&);
///         Tries to extracts an element from the front of the queue
///         if available. Returns true if successful, otherwise false.
///     bool try_dequeue_until(T&, time_point& deadline);
///         Tries to extracts an element from the front of the queue
///         if available until the specified daedline. Returns true
///         if successful. Otherwise Returns false.
///     bool try_dequeue_for(T&, duration&);
///         Tries to extracts an element from the front of the queue
///         if available until the expiration of the specified
///         duration.  Returns true if successful. Otherwise Returns
///         false.
///
///   Secondary functions:
///     void reset_capacity(size_t capacity);
///        Changes the capacity of the queue. Does not affect the
///        current contents of the queue. Guaranteed only to affect
///        subsequent enqueue operations. May or may not affect
///        concurrent operations. Capacity must be at least 1000.
///     Weight weight();
///        Returns an estimate of the total weight of the elements in
///        the queue.
///     size_t size();
///         Returns an estimate of the total number of elements.
///     bool empty();
///         Returns true only if the queue was empty during the call.
///     Note: weight(), size(), and empty() are guaranteed to be
///     accurate only if there are no concurrent changes to the queue.
///
/// Usage example with default weight:
/// @code
///   /* DMPSC, doesn't block, 1024 int elements per segment */
///   DMPSCQueue<int, false, 10> q(100000);
///   ASSERT_TRUE(q.empty());
///   ASSERT_EQ(q.size(), 0);
///   q.enqueue(1));
///   ASSERT_TRUE(q.try_enqueue(2));
///   ASSERT_TRUE(q.try_enqueue_until(3, deadline));
///   ASSERT_TRUE(q.try_enqueue(4, duration));
///   // ... enqueue more elements until capacity is full
///   // See above comments about imprecise capacity guarantees
///   ASSERT_FALSE(q.try_enqueue(100001)); // can't enqueue but can't wait
///   size_t sz = q.size();
///   ASSERT_GE(sz, 100000);
///   q.reset_capacity(1000000000); // set huge capacity
///   ASSERT_TRUE(q.try_enqueue(100001)); // now enqueue succeeds
///   q.reset_capacity(100000); // set capacity back to 100,000
///   ASSERT_FALSE(q.try_enqueue(100002));
///   ASSERT_EQ(q.size(), sz + 1);
///   int v;
///   q.dequeue(v);
///   ASSERT_EQ(v, 1);
///   ASSERT_TRUE(q.try_dequeue(v));
///   ASSERT_EQ(v, 2);
///   ASSERT_TRUE(q.try_dequeue_until(v, deadline));
///   ASSERT_EQ(v, 3);
///   ASSERT_TRUE(q.try_dequeue_for(v, duration));
///   ASSERT_EQ(v, 4);
///   ASSERT_EQ(q.size(), sz - 3);
/// @endcode
///
/// Usage example with custom weights:
/// @code
///   struct CustomWeightFn {
///     uint64_t operator()(int val) { return val / 100; }
///   };
///   DMPMCQueue<int, false, 10, CustomWeightFn> q(20);
///   ASSERT_TRUE(q.empty());
///   q.enqueue(100);
///   ASSERT_TRUE(q.try_enqueue(200));
///   ASSERT_TRUE(q.try_enqueue_until(500, now() + seconds(1)));
///   ASSERT_EQ(q.size(), 3);
///   ASSERT_EQ(q.weight(), 8);
///   ASSERT_FALSE(q.try_enqueue_for(1700, microseconds(1)));
///   q.reset_capacity(1000000); // set capacity to 1000000 instead of 20
///   ASSERT_TRUE(q.try_enqueue_for(1700, microseconds(1)));
///   q.reset_capacity(20); // set capacity to 20 again
///   ASSERT_FALSE(q.try_enqueue(100));
///   ASSERT_EQ(q.size(), 4);
///   ASSERT_EQ(q.weight(), 25);
///   int v;
///   q.dequeue(v);
///   ASSERT_EQ(v, 100);
///   ASSERT_TRUE(q.try_dequeue(v));
///   ASSERT_EQ(v, 200);
///   ASSERT_TRUE(q.try_dequeue_until(v, now() + seconds(1)));
///   ASSERT_EQ(v, 500);
///   ASSERT_EQ(q.size(), 1);
///   ASSERT_EQ(q.weight(), 17);
/// @endcode
///
/// Design:
/// - The implementation is on top of UnboundedQueue.
/// - The main FIFO functionality is in UnboundedQueue.
///   DynamicBoundedQueue manages keeping the total queue weight
///   within the specified capacity.
/// - For the sake of scalability, the data structures are designed to
///   minimize interference between producers on one side and
///   consumers on the other.
/// - Producers add to a debit variable the weight of the added
///   element and check capacity.
/// - Consumers add to a credit variable the weight of the removed
///   element.
/// - Producers, for the sake of scalability, use fetch_add to add to
///   the debit variable and subtract if it exceeded capacity,
///   rather than using compare_exchange to avoid overshooting.
/// - Consumers, infrequently, transfer credit to a transfer variable
///   and unblock any blocked producers. The transfer variable can be
///   used by producers to decrease their debit when needed.
/// - Note that a low capacity will trigger frequent credit transfer
///   by consumers that may degrade performance. Capacity should not
///   be set too low.
/// - Transfer of credit by consumers is triggered when the amount of
///   credit reaches a threshold (1/10 of capacity).
/// - The waiting of consumers is handled in UnboundedQueue.
///   The waiting of producers is handled in this template.
/// - For a producer operation, if the difference between debit and
///   capacity (plus some slack to account for the transfer threshold)
///   does not accommodate the weight of the new element, it first
///   tries to transfer credit that may have already been made
///   available by consumers. If this is insufficient and MayBlock is
///   true, then the producer uses a futex to block until new credit
///   is transferred by a consumer.
///
/// Memory Usage:
/// - Aside from three cache lines for managing capacity, the memory
///   for queue elements is managed using UnboundedQueue and grows and
///   shrinks dynamically with the number of elements.
/// - The template parameter LgAlign can be used to reduce memory usage
///   at the cost of increased chance of false sharing.

template <typename T>
struct DefaultWeightFn {
  template <typename Arg>
  uint64_t operator()(Arg&&) const noexcept {
    return 1;
  }
};

template <
    typename T,
    bool SingleProducer,
    bool SingleConsumer,
    bool MayBlock,
    size_t LgSegmentSize = 8,
    size_t LgAlign = 7,
    typename WeightFn = DefaultWeightFn<T>,
    template <typename> class Atom = std::atomic>
class DynamicBoundedQueue {
  using Weight = uint64_t;

  enum WaitingState : uint32_t {
    NOTWAITING = 0,
    WAITING = 1,
  };

  static constexpr bool SPSC = SingleProducer && SingleConsumer;
  static constexpr size_t Align = 1u << LgAlign;

  static_assert(LgAlign < 16, "LgAlign must be < 16");

  /// Data members

  // Read mostly by producers
  alignas(Align) Atom<Weight> debit_; // written frequently only by producers
  Atom<Weight> capacity_; // written rarely by capacity resets

  // Read mostly by consumers
  alignas(Align) Atom<Weight> credit_; // written frequently only by consumers
  Atom<Weight> threshold_; // written rarely only by capacity resets

  // Normally written and read rarely by producers and consumers
  // May be read frequently by producers when capacity is full
  alignas(Align) Atom<Weight> transfer_;
  detail::Futex<Atom> waiting_;

  // Underlying unbounded queue
  UnboundedQueue<
      T,
      SingleProducer,
      SingleConsumer,
      MayBlock,
      LgSegmentSize,
      LgAlign,
      Atom>
      q_;

 public:
  /** constructor */
  explicit DynamicBoundedQueue(Weight capacity)
      : debit_(0),
        capacity_(capacity + threshold(capacity)), // capacity slack
        credit_(0),
        threshold_(threshold(capacity)),
        transfer_(0),
        waiting_(0) {}

  /** destructor */
  ~DynamicBoundedQueue() {}

  /// Enqueue functions

  /** enqueue */
  FOLLY_ALWAYS_INLINE void enqueue(const T& v) { enqueueImpl(v); }

  FOLLY_ALWAYS_INLINE void enqueue(T&& v) { enqueueImpl(std::move(v)); }

  /** try_enqueue */
  FOLLY_ALWAYS_INLINE bool try_enqueue(const T& v) { return tryEnqueueImpl(v); }

  FOLLY_ALWAYS_INLINE bool try_enqueue(T&& v) {
    return tryEnqueueImpl(std::move(v));
  }

  /** try_enqueue_until */
  template <typename Clock, typename Duration>
  FOLLY_ALWAYS_INLINE bool try_enqueue_until(
      const T& v, const std::chrono::time_point<Clock, Duration>& deadline) {
    return tryEnqueueUntilImpl(v, deadline);
  }

  template <typename Clock, typename Duration>
  FOLLY_ALWAYS_INLINE bool try_enqueue_until(
      T&& v, const std::chrono::time_point<Clock, Duration>& deadline) {
    return tryEnqueueUntilImpl(std::move(v), deadline);
  }

  /** try_enqueue_for */
  template <typename Rep, typename Period>
  FOLLY_ALWAYS_INLINE bool try_enqueue_for(
      const T& v, const std::chrono::duration<Rep, Period>& duration) {
    return tryEnqueueForImpl(v, duration);
  }

  template <typename Rep, typename Period>
  FOLLY_ALWAYS_INLINE bool try_enqueue_for(
      T&& v, const std::chrono::duration<Rep, Period>& duration) {
    return tryEnqueueForImpl(std::move(v), duration);
  }

  /// Dequeue functions

  /** dequeue */
  FOLLY_ALWAYS_INLINE void dequeue(T& elem) {
    q_.dequeue(elem);
    addCredit(WeightFn()(elem));
  }

  /** try_dequeue */
  FOLLY_ALWAYS_INLINE bool try_dequeue(T& elem) {
    if (q_.try_dequeue(elem)) {
      addCredit(WeightFn()(elem));
      return true;
    }
    return false;
  }

  /** try_dequeue_until */
  template <typename Clock, typename Duration>
  FOLLY_ALWAYS_INLINE bool try_dequeue_until(
      T& elem, const std::chrono::time_point<Clock, Duration>& deadline) {
    if (q_.try_dequeue_until(elem, deadline)) {
      addCredit(WeightFn()(elem));
      return true;
    }
    return false;
  }

  /** try_dequeue_for */
  template <typename Rep, typename Period>
  FOLLY_ALWAYS_INLINE bool try_dequeue_for(
      T& elem, const std::chrono::duration<Rep, Period>& duration) {
    if (q_.try_dequeue_for(elem, duration)) {
      addCredit(WeightFn()(elem));
      return true;
    }
    return false;
  }

  /// Secondary functions

  /** reset_capacity */
  void reset_capacity(Weight capacity) noexcept {
    Weight thresh = threshold(capacity);
    capacity_.store(capacity + thresh, std::memory_order_release);
    threshold_.store(thresh, std::memory_order_release);
  }

  /** weight */
  Weight weight() const noexcept {
    auto d = getDebit();
    auto c = getCredit();
    auto t = getTransfer();
    return d > (c + t) ? d - (c + t) : 0;
  }

  /** size */
  size_t size() const noexcept { return q_.size(); }

  /** empty */
  bool empty() const noexcept { return q_.empty(); }

 private:
  /// Private functions ///

  // Calculation of threshold to move credits in bulk from consumers
  // to producers
  constexpr Weight threshold(Weight capacity) const noexcept {
    return capacity / 10;
  }

  // Functions called frequently by producers

  template <typename Arg>
  FOLLY_ALWAYS_INLINE void enqueueImpl(Arg&& v) {
    tryEnqueueUntilImpl(
        std::forward<Arg>(v), std::chrono::steady_clock::time_point::max());
  }

  template <typename Arg>
  FOLLY_ALWAYS_INLINE bool tryEnqueueImpl(Arg&& v) {
    return tryEnqueueUntilImpl(
        std::forward<Arg>(v), std::chrono::steady_clock::time_point::min());
  }

  template <typename Clock, typename Duration, typename Arg>
  FOLLY_ALWAYS_INLINE bool tryEnqueueUntilImpl(
      Arg&& v, const std::chrono::time_point<Clock, Duration>& deadline) {
    Weight weight = WeightFn()(std::forward<Arg>(v));
    if (LIKELY(tryAddDebit(weight))) {
      q_.enqueue(std::forward<Arg>(v));
      return true;
    }
    return tryEnqueueUntilSlow(std::forward<Arg>(v), deadline);
  }

  template <typename Rep, typename Period, typename Arg>
  FOLLY_ALWAYS_INLINE bool tryEnqueueForImpl(
      Arg&& v, const std::chrono::duration<Rep, Period>& duration) {
    if (LIKELY(tryEnqueueImpl(std::forward<Arg>(v)))) {
      return true;
    }
    auto deadline = std::chrono::steady_clock::now() + duration;
    return tryEnqueueUntilSlow(std::forward<Arg>(v), deadline);
  }

  FOLLY_ALWAYS_INLINE bool tryAddDebit(Weight weight) noexcept {
    Weight capacity = getCapacity();
    Weight before = fetchAddDebit(weight);
    if (LIKELY(before + weight <= capacity)) {
      return true;
    } else {
      subDebit(weight);
      return false;
    }
  }

  FOLLY_ALWAYS_INLINE Weight getCapacity() const noexcept {
    return capacity_.load(std::memory_order_acquire);
  }

  FOLLY_ALWAYS_INLINE Weight fetchAddDebit(Weight weight) noexcept {
    Weight before;
    if (SingleProducer) {
      before = getDebit();
      debit_.store(before + weight, std::memory_order_relaxed);
    } else {
      before = debit_.fetch_add(weight, std::memory_order_acq_rel);
    }
    return before;
  }

  FOLLY_ALWAYS_INLINE Weight getDebit() const noexcept {
    return debit_.load(std::memory_order_acquire);
  }

  // Functions called frequently by consumers

  FOLLY_ALWAYS_INLINE void addCredit(Weight weight) noexcept {
    Weight before = fetchAddCredit(weight);
    Weight thresh = getThreshold();
    if (before + weight >= thresh && before < thresh) {
      transferCredit();
    }
  }

  FOLLY_ALWAYS_INLINE Weight fetchAddCredit(Weight weight) noexcept {
    Weight before;
    if (SingleConsumer) {
      before = getCredit();
      credit_.store(before + weight, std::memory_order_relaxed);
    } else {
      before = credit_.fetch_add(weight, std::memory_order_acq_rel);
    }
    return before;
  }

  FOLLY_ALWAYS_INLINE Weight getCredit() const noexcept {
    return credit_.load(std::memory_order_acquire);
  }

  FOLLY_ALWAYS_INLINE Weight getThreshold() const noexcept {
    return threshold_.load(std::memory_order_acquire);
  }

  /** Functions called infrequently by producers */

  void subDebit(Weight weight) noexcept {
    Weight before;
    if (SingleProducer) {
      before = getDebit();
      debit_.store(before - weight, std::memory_order_relaxed);
    } else {
      before = debit_.fetch_sub(weight, std::memory_order_acq_rel);
    }
    DCHECK_GE(before, weight);
  }

  template <typename Clock, typename Duration, typename Arg>
  bool tryEnqueueUntilSlow(
      Arg&& v, const std::chrono::time_point<Clock, Duration>& deadline) {
    Weight weight = WeightFn()(std::forward<Arg>(v));
    if (canEnqueue(deadline, weight)) {
      q_.enqueue(std::forward<Arg>(v));
      return true;
    } else {
      return false;
    }
  }

  template <typename Clock, typename Duration>
  bool canEnqueue(
      const std::chrono::time_point<Clock, Duration>& deadline,
      Weight weight) noexcept {
    Weight capacity = getCapacity();
    while (true) {
      tryReduceDebit();
      Weight debit = getDebit();
      if ((debit + weight <= capacity) && tryAddDebit(weight)) {
        return true;
      }
      if (deadline < Clock::time_point::max() && Clock::now() >= deadline) {
        return false;
      }
      if (MayBlock) {
        if (canBlock(weight, capacity)) {
          detail::futexWaitUntil(&waiting_, WAITING, deadline);
        }
      } else {
        asm_volatile_pause();
      }
    }
  }

  bool canBlock(Weight weight, Weight capacity) noexcept {
    waiting_.store(WAITING, std::memory_order_relaxed);
    std::atomic_thread_fence(std::memory_order_seq_cst);
    tryReduceDebit();
    Weight debit = getDebit();
    return debit + weight > capacity;
  }

  bool tryReduceDebit() noexcept {
    Weight w = takeTransfer();
    if (w > 0) {
      subDebit(w);
    }
    return w > 0;
  }

  Weight takeTransfer() noexcept {
    Weight w = getTransfer();
    if (w > 0) {
      w = transfer_.exchange(0, std::memory_order_acq_rel);
    }
    return w;
  }

  Weight getTransfer() const noexcept {
    return transfer_.load(std::memory_order_acquire);
  }

  /** Functions called infrequently by consumers */

  void transferCredit() noexcept {
    Weight credit = takeCredit();
    transfer_.fetch_add(credit, std::memory_order_acq_rel);
    if (MayBlock) {
      std::atomic_thread_fence(std::memory_order_seq_cst);
      waiting_.store(NOTWAITING, std::memory_order_relaxed);
      detail::futexWake(&waiting_);
    }
  }

  Weight takeCredit() noexcept {
    Weight credit;
    if (SingleConsumer) {
      credit = credit_.load(std::memory_order_relaxed);
      credit_.store(0, std::memory_order_relaxed);
    } else {
      credit = credit_.exchange(0, std::memory_order_acq_rel);
    }
    return credit;
  }

}; // DynamicBoundedQueue

/// Aliases

/** DSPSCQueue */
template <
    typename T,
    bool MayBlock,
    size_t LgSegmentSize = 8,
    size_t LgAlign = 7,
    typename WeightFn = DefaultWeightFn<T>,
    template <typename> class Atom = std::atomic>
using DSPSCQueue = DynamicBoundedQueue<
    T,
    true,
    true,
    MayBlock,
    LgSegmentSize,
    LgAlign,
    WeightFn,
    Atom>;

/** DMPSCQueue */
template <
    typename T,
    bool MayBlock,
    size_t LgSegmentSize = 8,
    size_t LgAlign = 7,
    typename WeightFn = DefaultWeightFn<T>,
    template <typename> class Atom = std::atomic>
using DMPSCQueue = DynamicBoundedQueue<
    T,
    false,
    true,
    MayBlock,
    LgSegmentSize,
    LgAlign,
    WeightFn,
    Atom>;

/** DSPMCQueue */
template <
    typename T,
    bool MayBlock,
    size_t LgSegmentSize = 8,
    size_t LgAlign = 7,
    typename WeightFn = DefaultWeightFn<T>,
    template <typename> class Atom = std::atomic>
using DSPMCQueue = DynamicBoundedQueue<
    T,
    true,
    false,
    MayBlock,
    LgSegmentSize,
    LgAlign,
    WeightFn,
    Atom>;

/** DMPMCQueue */
template <
    typename T,
    bool MayBlock,
    size_t LgSegmentSize = 8,
    size_t LgAlign = 7,
    typename WeightFn = DefaultWeightFn<T>,
    template <typename> class Atom = std::atomic>
using DMPMCQueue = DynamicBoundedQueue<
    T,
    false,
    false,
    MayBlock,
    LgSegmentSize,
    LgAlign,
    WeightFn,
    Atom>;

} // namespace folly
