content: /*
   * Copyright 1999-2018 Alibaba Group Holding Ltd.
   *
   * 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.
   */
  package com.alibaba.csp.sentinel.slots.block.flow.controller;
  
  import java.util.concurrent.atomic.AtomicLong;
  
  import com.alibaba.csp.sentinel.util.TimeUtil;
  import com.alibaba.csp.sentinel.node.Node;
  import com.alibaba.csp.sentinel.slots.block.flow.TrafficShapingController;
  
  /**
   * <p>
   * The principle idea comes from Guava. However, the calculation of Guava is
   * rate-based, which means that we need to translate rate to QPS.
   * </p>
   *
   * <p>
   * Requests arriving at the pulse may drag down long idle systems even though it
   * has a much larger handling capability in stable period. It usually happens in
   * scenarios that require extra time for initialization, e.g. DB establishes a connection,
   * connects to a remote service, and so on. That’s why we need “warm up”.
   * </p>
   *
   * <p>
   * Sentinel's "warm-up" implementation is based on the Guava's algorithm.
   * However, Guava’s implementation focuses on adjusting the request interval,
   * which is similar to leaky bucket. Sentinel pays more attention to
   * controlling the count of incoming requests per second without calculating its interval,
   * which resembles token bucket algorithm.
   * </p>
   *
   * <p>
   * The remaining tokens in the bucket is used to measure the system utility.
   * Suppose a system can handle b requests per second. Every second b tokens will
   * be added into the bucket until the bucket is full. And when system processes
   * a request, it takes a token from the bucket. The more tokens left in the
   * bucket, the lower the utilization of the system; when the token in the token
   * bucket is above a certain threshold, we call it in a "saturation" state.
   * </p>
   *
   * <p>
   * Base on Guava’s theory, there is a linear equation we can write this in the
   * form y = m * x + b where y (a.k.a y(x)), or qps(q)), is our expected QPS
   * given a saturated period (e.g. 3 minutes in), m is the rate of change from
   * our cold (minimum) rate to our stable (maximum) rate, x (or q) is the
   * occupied token.
   * </p>
   *
   * @author jialiang.linjl
   */
  public class WarmUpController implements TrafficShapingController {
  
      protected double count;
      private int coldFactor;
      protected int warningToken = 0;
      private int maxToken;
      protected double slope;
  
      protected AtomicLong storedTokens = new AtomicLong(0);
      protected AtomicLong lastFilledTime = new AtomicLong(0);
  
      public WarmUpController(double count, int warmUpPeriodInSec, int coldFactor) {
          construct(count, warmUpPeriodInSec, coldFactor);
      }
  
      public WarmUpController(double count, int warmUpPeriodInSec) {
          construct(count, warmUpPeriodInSec, 3);
      }
  
      private void construct(double count, int warmUpPeriodInSec, int coldFactor) {
  
          if (coldFactor <= 1) {
              throw new IllegalArgumentException("Cold factor should be larger than 1");
          }
  
          this.count = count;
  
          this.coldFactor = coldFactor;
  
          // thresholdPermits = 0.5 * warmupPeriod / stableInterval.
          // warningToken = 100;
          warningToken = (int)(warmUpPeriodInSec * count) / (coldFactor - 1);
          // / maxPermits = thresholdPermits + 2 * warmupPeriod /
          // (stableInterval + coldInterval)
          // maxToken = 200
          maxToken = warningToken + (int)(2 * warmUpPeriodInSec * count / (1.0 + coldFactor));
  
          // slope
          // slope = (coldIntervalMicros - stableIntervalMicros) / (maxPermits
          // - thresholdPermits);
          slope = (coldFactor - 1.0) / count / (maxToken - warningToken);
  
      }
  
      @Override
      public boolean canPass(Node node, int acquireCount) {
          return canPass(node, acquireCount, false);
      }
  
      @Override
      public boolean canPass(Node node, int acquireCount, boolean prioritized) {
          long passQps = (long) node.passQps();
  
          long previousQps = (long) node.previousPassQps();
          syncToken(previousQps);
  
          // 开始计算它的斜率
          // 如果进入了警戒线，开始调整他的qps
          long restToken = storedTokens.get();
          if (restToken >= warningToken) {
              long aboveToken = restToken - warningToken;
              // 消耗的速度要比warning快，但是要比慢
              // current interval = restToken*slope+1/count
              double warningQps = Math.nextUp(1.0 / (aboveToken * slope + 1.0 / count));
              if (passQps + acquireCount <= warningQps) {
                  return true;
              }
          } else {
              if (passQps + acquireCount <= count) {
                  return true;
              }
          }
  
          return false;
      }
  
      protected void syncToken(long passQps) {
          long currentTime = TimeUtil.currentTimeMillis();
          currentTime = currentTime - currentTime % 1000;
          long oldLastFillTime = lastFilledTime.get();
          if (currentTime <= oldLastFillTime) {
              return;
          }
  
          long oldValue = storedTokens.get();
          long newValue = coolDownTokens(currentTime, passQps);
  
          if (storedTokens.compareAndSet(oldValue, newValue)) {
              long currentValue = storedTokens.addAndGet(0 - passQps);
              if (currentValue < 0) {
                  storedTokens.set(0L);
              }
              lastFilledTime.set(currentTime);
          }
  
      }
  
      private long coolDownTokens(long currentTime, long passQps) {
          long oldValue = storedTokens.get();
          long newValue = oldValue;
  
          // 添加令牌的判断前提条件:
          // 当令牌的消耗程度远远低于警戒线的时候
          if (oldValue < warningToken) {
              newValue = (long)(oldValue + (currentTime - lastFilledTime.get()) * count / 1000);
          } else if (oldValue > warningToken) {
              if (passQps < (int)count / coldFactor) {
                  newValue = (long)(oldValue + (currentTime - lastFilledTime.get()) * count / 1000);
              }
          }
          return Math.min(newValue, maxToken);
      }
  
  }
  